Skip to main content

max / makenotwork

7.0 KB · 221 lines History Blame Raw
1 //! Git repositories and the SSH keys that reach them.
2 //!
3 //! Addressed by repo NAME, matching the CLI's own vocabulary. The browser API
4 //! keys on the repo id because a page has the row loaded; a person at a
5 //! terminal does not.
6
7 use super::{MnwApiClient, empty_response, json_response};
8 use serde::{Deserialize, Serialize};
9
10 /// A repository as `repo list` renders it.
11 #[derive(Debug, Clone, Deserialize, Serialize)]
12 pub(crate) struct CliRepo {
13 pub name: String,
14 pub visibility: String,
15 pub description: String,
16 pub created_at: String,
17 }
18
19 /// A repository plus its issue counts, for `repo info`.
20 #[derive(Debug, Clone, Deserialize, Serialize)]
21 pub(crate) struct CliRepoInfo {
22 pub name: String,
23 pub visibility: String,
24 pub description: String,
25 pub created_at: String,
26 pub open_issues: i64,
27 pub closed_issues: i64,
28 }
29
30 /// An SSH key as `key list` renders it.
31 #[derive(Debug, Clone, Deserialize, Serialize)]
32 pub(crate) struct CliSshKey {
33 pub fingerprint: String,
34 pub label: String,
35 pub created_at: String,
36 }
37
38 /// A registered SSH key.
39 #[derive(Debug, Clone, Deserialize, Serialize)]
40 pub(crate) struct SshKeyInfo {
41 pub id: String,
42 pub label: String,
43 pub fingerprint: String,
44 pub created_at: String,
45 }
46
47 /// Response from the git authorize endpoint.
48 #[derive(Debug, Deserialize)]
49 pub(crate) struct GitAuthResponse {
50 pub repo_path: String,
51 }
52
53 impl MnwApiClient {
54 /// Authorize a git operation and get the on-disk repo path.
55 pub(crate) async fn git_authorize(
56 &self,
57 user_id: &str,
58 operation: &str,
59 owner: &str,
60 repo_name: &str,
61 ) -> anyhow::Result<GitAuthResponse> {
62 let url = format!("{}/api/internal/git/authorize", self.base_url);
63 let resp = self
64 .http
65 .post(&url)
66 .bearer_auth(&self.service_token)
67 .header("X-MNW-Actor", self.actor_header())
68 .json(&serde_json::json!({
69 "user_id": user_id,
70 "operation": operation,
71 "owner": owner,
72 "repo_name": repo_name,
73 }))
74 .send()
75 .await?;
76
77 if !resp.status().is_success() {
78 let status = resp.status();
79 let body = resp.text().await.unwrap_or_else(|e| {
80 tracing::warn!(error = %e, "failed to read git_authorize error body");
81 String::new()
82 });
83 // Parse JSON error if available, fall back to status text
84 let msg = serde_json::from_str::<serde_json::Value>(&body)
85 .ok()
86 .and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from))
87 .unwrap_or_else(|| format!("HTTP {status}"));
88 anyhow::bail!("{msg}");
89 }
90
91 Ok(resp.json().await?)
92 }
93
94 /// List registered SSH keys for a user.
95 pub(crate) async fn list_ssh_keys(&self, user_id: &str) -> anyhow::Result<Vec<SshKeyInfo>> {
96 let url = format!("{}/api/internal/creator/ssh-keys", self.base_url);
97 let resp = self
98 .http
99 .get(&url)
100 .bearer_auth(&self.service_token)
101 .header("X-MNW-Actor", self.actor_header())
102 .query(&[("user_id", user_id)])
103 .send()
104 .await?;
105
106 json_response(resp, "list_ssh_keys").await
107 }
108
109 pub(crate) async fn repo_list(&self, user_id: &str) -> anyhow::Result<Vec<CliRepo>> {
110 let url = format!("{}/api/internal/creator/repos", self.base_url);
111 let resp = self
112 .http
113 .get(&url)
114 .bearer_auth(&self.service_token)
115 .header("X-MNW-Actor", self.actor_header())
116 .query(&[("user_id", user_id)])
117 .send()
118 .await?;
119 json_response(resp, "repo_list").await
120 }
121
122 pub(crate) async fn repo_info(&self, user_id: &str, name: &str) -> anyhow::Result<CliRepoInfo> {
123 let url = format!("{}/api/internal/creator/repos/{name}", self.base_url);
124 let resp = self
125 .http
126 .get(&url)
127 .bearer_auth(&self.service_token)
128 .header("X-MNW-Actor", self.actor_header())
129 .query(&[("user_id", user_id)])
130 .send()
131 .await?;
132 json_response(resp, "repo_info").await
133 }
134
135 pub(crate) async fn repo_set_visibility(
136 &self,
137 user_id: &str,
138 name: &str,
139 visibility: &str,
140 ) -> anyhow::Result<()> {
141 let url = format!(
142 "{}/api/internal/creator/repos/{name}/visibility",
143 self.base_url
144 );
145 let resp = self
146 .http
147 .put(&url)
148 .bearer_auth(&self.service_token)
149 .header("X-MNW-Actor", self.actor_header())
150 .query(&[("user_id", user_id)])
151 .json(&serde_json::json!({ "visibility": visibility }))
152 .send()
153 .await?;
154 empty_response(resp, "repo_set_visibility").await
155 }
156
157 pub(crate) async fn repo_set_description(
158 &self,
159 user_id: &str,
160 name: &str,
161 description: &str,
162 ) -> anyhow::Result<()> {
163 let url = format!(
164 "{}/api/internal/creator/repos/{name}/description",
165 self.base_url
166 );
167 let resp = self
168 .http
169 .put(&url)
170 .bearer_auth(&self.service_token)
171 .header("X-MNW-Actor", self.actor_header())
172 .query(&[("user_id", user_id)])
173 .json(&serde_json::json!({ "description": description }))
174 .send()
175 .await?;
176 empty_response(resp, "repo_set_description").await
177 }
178
179 pub(crate) async fn repo_delete(&self, user_id: &str, name: &str) -> anyhow::Result<()> {
180 let url = format!("{}/api/internal/creator/repos/{name}", self.base_url);
181 let resp = self
182 .http
183 .delete(&url)
184 .bearer_auth(&self.service_token)
185 .header("X-MNW-Actor", self.actor_header())
186 .query(&[("user_id", user_id)])
187 .send()
188 .await?;
189 empty_response(resp, "repo_delete").await
190 }
191
192 pub(crate) async fn key_list(&self, user_id: &str) -> anyhow::Result<Vec<CliSshKey>> {
193 let url = format!("{}/api/internal/creator/ssh-keys", self.base_url);
194 let resp = self
195 .http
196 .get(&url)
197 .bearer_auth(&self.service_token)
198 .header("X-MNW-Actor", self.actor_header())
199 .query(&[("user_id", user_id)])
200 .send()
201 .await?;
202 json_response(resp, "key_list").await
203 }
204
205 pub(crate) async fn key_remove(&self, user_id: &str, fingerprint: &str) -> anyhow::Result<()> {
206 let url = format!(
207 "{}/api/internal/creator/ssh-keys/{fingerprint}",
208 self.base_url
209 );
210 let resp = self
211 .http
212 .delete(&url)
213 .bearer_auth(&self.service_token)
214 .header("X-MNW-Actor", self.actor_header())
215 .query(&[("user_id", user_id)])
216 .send()
217 .await?;
218 empty_response(resp, "key_remove").await
219 }
220 }
221