this repo has no description
1use crate::state::AppState; 2use crate::sync::util::{AccountStatus, assert_repo_availability, get_account_with_status}; 3use axum::{ 4 Json, 5 extract::{Query, State}, 6 http::StatusCode, 7 response::{IntoResponse, Response}, 8}; 9use cid::Cid; 10use jacquard_repo::commit::Commit; 11use jacquard_repo::storage::BlockStore; 12use serde::{Deserialize, Serialize}; 13use serde_json::json; 14use std::str::FromStr; 15use tracing::error; 16 17async fn get_rev_from_commit(state: &AppState, cid_str: &str) -> Option<String> { 18 let cid = Cid::from_str(cid_str).ok()?; 19 let block = state.block_store.get(&cid).await.ok()??; 20 let commit = Commit::from_cbor(&block).ok()?; 21 Some(commit.rev().to_string()) 22} 23 24#[derive(Deserialize)] 25pub struct GetLatestCommitParams { 26 pub did: String, 27} 28 29#[derive(Serialize)] 30pub struct GetLatestCommitOutput { 31 pub cid: String, 32 pub rev: String, 33} 34 35pub async fn get_latest_commit( 36 State(state): State<AppState>, 37 Query(params): Query<GetLatestCommitParams>, 38) -> Response { 39 let did = params.did.trim(); 40 if did.is_empty() { 41 return ( 42 StatusCode::BAD_REQUEST, 43 Json(json!({"error": "InvalidRequest", "message": "did is required"})), 44 ) 45 .into_response(); 46 } 47 48 let account = match assert_repo_availability(&state.db, did, false).await { 49 Ok(a) => a, 50 Err(e) => return e.into_response(), 51 }; 52 53 let repo_root_cid = match account.repo_root_cid { 54 Some(cid) => cid, 55 None => { 56 return ( 57 StatusCode::BAD_REQUEST, 58 Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), 59 ) 60 .into_response(); 61 } 62 }; 63 64 let rev = match get_rev_from_commit(&state, &repo_root_cid).await { 65 Some(r) => r, 66 None => { 67 error!( 68 "Failed to parse commit for DID {}: CID {}", 69 did, repo_root_cid 70 ); 71 return ( 72 StatusCode::INTERNAL_SERVER_ERROR, 73 Json(json!({"error": "InternalError", "message": "Failed to read repo commit"})), 74 ) 75 .into_response(); 76 } 77 }; 78 79 ( 80 StatusCode::OK, 81 Json(GetLatestCommitOutput { 82 cid: repo_root_cid, 83 rev, 84 }), 85 ) 86 .into_response() 87} 88 89#[derive(Deserialize)] 90pub struct ListReposParams { 91 pub limit: Option<i64>, 92 pub cursor: Option<String>, 93} 94 95#[derive(Serialize)] 96#[serde(rename_all = "camelCase")] 97pub struct RepoInfo { 98 pub did: String, 99 pub head: String, 100 pub rev: String, 101 pub active: bool, 102 #[serde(skip_serializing_if = "Option::is_none")] 103 pub status: Option<String>, 104} 105 106#[derive(Serialize)] 107pub struct ListReposOutput { 108 #[serde(skip_serializing_if = "Option::is_none")] 109 pub cursor: Option<String>, 110 pub repos: Vec<RepoInfo>, 111} 112 113pub async fn list_repos( 114 State(state): State<AppState>, 115 Query(params): Query<ListReposParams>, 116) -> Response { 117 let limit = params.limit.unwrap_or(50).clamp(1, 1000); 118 let cursor_did = params.cursor.as_deref().unwrap_or(""); 119 let result = sqlx::query!( 120 r#" 121 SELECT u.did, u.deactivated_at, u.takedown_ref, r.repo_root_cid, r.repo_rev 122 FROM repos r 123 JOIN users u ON r.user_id = u.id 124 WHERE u.did > $1 125 ORDER BY u.did ASC 126 LIMIT $2 127 "#, 128 cursor_did, 129 limit + 1 130 ) 131 .fetch_all(&state.db) 132 .await; 133 match result { 134 Ok(rows) => { 135 let has_more = rows.len() as i64 > limit; 136 let mut repos: Vec<RepoInfo> = Vec::new(); 137 for row in rows.iter().take(limit as usize) { 138 let rev = match get_rev_from_commit(&state, &row.repo_root_cid).await { 139 Some(r) => r, 140 None => { 141 if let Some(ref stored_rev) = row.repo_rev { 142 stored_rev.clone() 143 } else { 144 tracing::warn!( 145 "Failed to parse commit for DID {} in list_repos: CID {}", 146 row.did, 147 row.repo_root_cid 148 ); 149 continue; 150 } 151 } 152 }; 153 let status = if row.takedown_ref.is_some() { 154 AccountStatus::Takendown 155 } else if row.deactivated_at.is_some() { 156 AccountStatus::Deactivated 157 } else { 158 AccountStatus::Active 159 }; 160 repos.push(RepoInfo { 161 did: row.did.clone(), 162 head: row.repo_root_cid.clone(), 163 rev, 164 active: status.is_active(), 165 status: status.as_str().map(String::from), 166 }); 167 } 168 let next_cursor = if has_more { 169 repos.last().map(|r| r.did.clone()) 170 } else { 171 None 172 }; 173 ( 174 StatusCode::OK, 175 Json(ListReposOutput { 176 cursor: next_cursor, 177 repos, 178 }), 179 ) 180 .into_response() 181 } 182 Err(e) => { 183 error!("DB error in list_repos: {:?}", e); 184 ( 185 StatusCode::INTERNAL_SERVER_ERROR, 186 Json(json!({"error": "InternalError"})), 187 ) 188 .into_response() 189 } 190 } 191} 192 193#[derive(Deserialize)] 194pub struct GetRepoStatusParams { 195 pub did: String, 196} 197 198#[derive(Serialize)] 199pub struct GetRepoStatusOutput { 200 pub did: String, 201 pub active: bool, 202 #[serde(skip_serializing_if = "Option::is_none")] 203 pub status: Option<String>, 204 #[serde(skip_serializing_if = "Option::is_none")] 205 pub rev: Option<String>, 206} 207 208pub async fn get_repo_status( 209 State(state): State<AppState>, 210 Query(params): Query<GetRepoStatusParams>, 211) -> Response { 212 let did = params.did.trim(); 213 if did.is_empty() { 214 return ( 215 StatusCode::BAD_REQUEST, 216 Json(json!({"error": "InvalidRequest", "message": "did is required"})), 217 ) 218 .into_response(); 219 } 220 221 let account = match get_account_with_status(&state.db, did).await { 222 Ok(Some(a)) => a, 223 Ok(None) => { 224 return ( 225 StatusCode::BAD_REQUEST, 226 Json(json!({"error": "RepoNotFound", "message": format!("Could not find repo for DID: {}", did)})), 227 ) 228 .into_response() 229 } 230 Err(e) => { 231 error!("DB error in get_repo_status: {:?}", e); 232 return ( 233 StatusCode::INTERNAL_SERVER_ERROR, 234 Json(json!({"error": "InternalError"})), 235 ) 236 .into_response(); 237 } 238 }; 239 240 let rev = if account.status.is_active() { 241 if let Some(ref cid) = account.repo_root_cid { 242 get_rev_from_commit(&state, cid).await 243 } else { 244 None 245 } 246 } else { 247 None 248 }; 249 250 ( 251 StatusCode::OK, 252 Json(GetRepoStatusOutput { 253 did: account.did, 254 active: account.status.is_active(), 255 status: account.status.as_str().map(String::from), 256 rev, 257 }), 258 ) 259 .into_response() 260}