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}