1use super::GitLab;
2use flams_utils::unwrap;
3use ftml_uris::ArchiveId;
4use gitlab::api::AsyncQuery;
5pub use oauth2::AccessToken;
6use oauth2::{url::Url, TokenResponse};
7use tracing::{instrument, Instrument};
8
9#[derive(Debug, Clone)]
10pub struct GitLabOAuth(
11 oauth2::basic::BasicClient<
12 oauth2::EndpointSet,
13 oauth2::EndpointNotSet,
14 oauth2::EndpointNotSet,
15 oauth2::EndpointNotSet,
16 oauth2::EndpointSet,
17 >,
18 GitLab,
19);
20
21#[derive(Debug, Clone, serde::Deserialize)]
22pub struct AuthRequest {
23 pub code: String,
24 }
26
27impl GitLabOAuth {
28 pub fn login_url(&self) -> Url {
29 let (url, _) = self
30 .0
31 .authorize_url(oauth2::CsrfToken::new_random)
32 .add_scope(oauth2::Scope::new("read_user".to_string()))
33 .add_scope(oauth2::Scope::new("api".to_string()))
34 .url();
35 url
36 }
37 pub async fn callback(
39 &self,
40 request: AuthRequest,
41 ) -> Result<AccessToken, impl std::error::Error> {
42 use oauth2::AuthorizationCode;
43 let code = AuthorizationCode::new(request.code);
44 let req = unwrap!(oauth2::reqwest::ClientBuilder::new().build().ok());
45 self.0
46 .exchange_code(code)
47 .request_async(&req)
48 .instrument(crate::REMOTE_SPAN.clone())
49 .await
50 .map(|token| token.access_token().clone())
51 }
52
53 pub async fn get_projects(
55 &self,
56 token: String,
57 ) -> Result<Vec<flams_backend_types::git::Project>, super::Err> {
58 self.get_projects_i(token)
59 .instrument(crate::REMOTE_SPAN.clone())
60 .await
61 }
62 #[instrument(
63 level = "debug",
64 target = "git",
65 name = "getting all projects for user",
66 skip_all
67 )]
68 async fn get_projects_i(
69 &self,
70 token: String,
71 ) -> Result<Vec<flams_backend_types::git::Project>, super::Err> {
72 let mut client = gitlab::ImpersonationClient::new(&self.1 .0.inner, token);
73 client.oauth2_token();
74 let r = gitlab::api::projects::Projects::builder()
75 .simple(true)
76 .min_access_level(gitlab::api::common::AccessLevel::Developer)
77 .build()
79 .unwrap_or_else(|_| unreachable!());
80 let r: Vec<flams_backend_types::git::Project> =
81 gitlab::api::paged(r, gitlab::api::Pagination::All)
82 .query_async(&client)
83 .await?;
84 let mut vs = self.1 .0.projects.lock();
85 for p in &r {
86 if !vs.contains(&p.id) {
87 vs.insert(super::ProjectWithId {
88 project: p.clone(),
89 id: None,
90 });
91 }
92 }
93 drop(vs);
94 Ok(r)
95 }
96
97 pub async fn get_archive_id(
99 &self,
100 id: u64,
101 token: String,
102 branch: &str,
103 ) -> Result<Option<ArchiveId>, super::Err> {
104 self.get_archive_id_i(id, token, branch)
105 .instrument(crate::REMOTE_SPAN.clone())
106 .await
107 }
108
109 #[instrument(
111 level = "debug",
112 target = "git",
113 name = "getting archive id",
114 skip(self, token)
115 )]
116 pub async fn get_archive_id_i(
117 &self,
118 id: u64,
119 token: String,
120 branch: &str,
121 ) -> Result<Option<ArchiveId>, super::Err> {
122 {
123 let vs = self.1 .0.projects.lock();
124 if let Some(super::ProjectWithId { id: Some(id), .. }) = vs.get(&id) {
125 return Ok(id.clone());
126 }
127 }
128
129 macro_rules! ret {
130 ($v:expr) => {{
131 tracing::info!("Found {:?}", $v);
132 let mut lock = self.1 .0.projects.lock();
133 if let Some(mut v) = lock.take(&id) {
134 v.id = Some($v.clone());
135 lock.insert(v);
136 }
137 drop(lock);
138 return Ok($v);
139 }};
140 }
141 let mut client = gitlab::ImpersonationClient::new(&self.1 .0.inner, token);
142 client.oauth2_token();
143 let r = gitlab::api::projects::repository::TreeBuilder::default()
144 .project(id)
145 .ref_(branch)
146 .recursive(false)
147 .build()
148 .unwrap_or_else(|_| unreachable!());
149 let r: Vec<flams_backend_types::git::TreeEntry> = r.query_async(&client).await?;
150 let Some(p) = r.into_iter().find_map(|e| {
151 if e.path.eq_ignore_ascii_case("meta-inf")
152 && matches!(e.tp, flams_backend_types::git::DirOrFile::Dir)
153 {
154 Some(e.path)
155 } else {
156 None
157 }
158 }) else {
159 ret!(None::<ArchiveId>)
160 };
161
162 let r = gitlab::api::projects::repository::TreeBuilder::default()
163 .project(id)
164 .ref_(branch)
165 .path(p)
166 .recursive(false)
167 .build()
168 .unwrap_or_else(|_| unreachable!());
169 let r: Vec<flams_backend_types::git::TreeEntry> = r.query_async(&client).await?;
170 let Some(p) = r.into_iter().find_map(|e| {
171 if e.name.eq_ignore_ascii_case("manifest.mf")
172 && matches!(e.tp, flams_backend_types::git::DirOrFile::File)
173 {
174 Some(e.path)
175 } else {
176 None
177 }
178 }) else {
179 ret!(None::<ArchiveId>)
180 };
181
182 let blob = gitlab::api::projects::repository::files::FileRaw::builder()
183 .project(id)
184 .file_path(p)
185 .ref_(branch)
186 .build()
187 .unwrap_or_else(|_| unreachable!());
188 let r = gitlab::api::raw(blob).query_async(&client).await?;
189 let r = std::str::from_utf8(&r)?;
190 let r = r.split('\n').find_map(|line| {
191 let line = line.trim();
192 line.strip_prefix("id:")
193 .and_then(|rest| ArchiveId::new(rest.trim()).ok())
194 });
195 ret!(r)
196 }
197
198 pub async fn get_branches(
200 &self,
201 id: u64,
202 token: String,
203 ) -> Result<Vec<flams_backend_types::git::Branch>, super::Err> {
204 self.get_branches_i(id, token)
205 .instrument(crate::REMOTE_SPAN.clone())
206 .await
207 }
208
209 #[instrument(
211 level = "debug",
212 target = "git",
213 name = "getting branches for archive",
214 skip(self, token)
215 )]
216 pub async fn get_branches_i(
217 &self,
218 id: u64,
219 token: String,
220 ) -> Result<Vec<flams_backend_types::git::Branch>, super::Err> {
221 let mut client = gitlab::ImpersonationClient::new(&self.1 .0.inner, token);
222 client.oauth2_token();
223 let r = gitlab::api::projects::repository::branches::Branches::builder()
224 .project(id)
225 .build()
226 .map_err(|e| super::Err::Other(e.to_string()))?
227 .query_async(&client)
228 .await?;
229 Ok(r)
230 }
231}
232
233impl GitLab {
234 pub fn new_oauth(&self, redirect: &str) -> Result<GitLabOAuth, OAuthError> {
236 use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
237 let url = &self.0.url;
238 let Some(app_id) = &self.0.id else {
239 return Err(OAuthError::MissingAppID);
240 };
241 let Some(app_secret) = &self.0.secret else {
242 return Err(OAuthError::MissingAppSecret);
243 };
244 let Ok(auth_url) = AuthUrl::new(format!("{url}/oauth/authorize")) else {
245 return Err(OAuthError::InvalidAuthURL(format!("{url}/oauth/authorize")));
246 };
247 let Ok(token_url) = TokenUrl::new(format!("{url}/oauth/token")) else {
248 return Err(OAuthError::InvalidTokenURL(format!("{url}/oauth/token")));
249 };
250 let Ok(redirect_url) = RedirectUrl::new(redirect.to_string()) else {
251 return Err(OAuthError::InvalidRedirectURL(redirect.to_string()));
252 };
253 let auth = oauth2::basic::BasicClient::new(ClientId::new(app_id.to_string()))
254 .set_client_secret(ClientSecret::new(app_secret.to_string()))
255 .set_auth_uri(auth_url)
256 .set_token_uri(token_url)
257 .set_redirect_uri(redirect_url);
258 Ok(GitLabOAuth(auth, self.clone()))
259 }
260
261 pub async fn get_oauth_user(
263 &self,
264 token: &oauth2::AccessToken,
265 ) -> Result<GitlabUser, reqwest::Error> {
266 let client = reqwest::Client::new();
267 let resp = client
268 .get(format!("{}/api/v4/user", self.0.url))
269 .bearer_auth(token.secret())
270 .send()
271 .instrument(crate::REMOTE_SPAN.clone())
272 .await?;
273 resp.json().instrument(crate::REMOTE_SPAN.clone()).await
274 }
275}
276
277#[derive(Debug)]
278pub enum OAuthError {
279 InvalidAuthURL(String),
280 InvalidTokenURL(String),
281 InvalidRedirectURL(String),
282 MissingAppID,
283 MissingAppSecret,
284}
285impl std::fmt::Display for OAuthError {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 match self {
288 Self::InvalidAuthURL(s) => write!(f, "Invalid auth url: {s}"),
289 Self::InvalidTokenURL(s) => write!(f, "Invalid token url: {s}"),
290 Self::InvalidRedirectURL(s) => write!(f, "Invalid redirect url: {s}"),
291 Self::MissingAppID => write!(f, "Missing app id"),
292 Self::MissingAppSecret => write!(f, "Missing secret"),
293 }
294 }
295}
296
297impl std::error::Error for OAuthError {}
298
299#[derive(Debug, serde::Deserialize, serde::Serialize)]
300pub struct GitlabUser {
301 pub id: i64,
302 pub username: String,
303 pub name: String,
304 pub avatar_url: String,
306 pub email: Option<String>,
308 pub can_create_group: bool,
310 pub can_create_project: bool,
311}
312
313