Git hosting and a container registry in one Rust binary (axum + Astro)
Importer: resolve the token owner's own account via /user so flagged accounts can be imported
2 files changed, +26 -5
+23-0backend/src/importer/github.rs
| @@ -162,6 +162,29 @@ impl GitHub { | ||
| 162 | 162 | .map_err(|e| if e.to_string() == "not found" { anyhow!("GitHub has no user or organization named '{login}'") } else { e }) |
| 163 | 163 | } |
| 164 | 164 | |
| 165 | + /// The account to import and the token's owner. When the token belongs to | |
| 166 | + /// the requested login, `/user` answers for it: that also works for | |
| 167 | + /// accounts GitHub has flagged, whose `/users/{login}` is a 404 to | |
| 168 | + /// everyone, the owner included. | |
| 169 | + pub async fn resolve(&self, login: &str) -> anyhow::Result<(RawAccount, Option<RawAccount>)> { | |
| 170 | + let login = login.trim(); | |
| 171 | + if !valid_login(login) { | |
| 172 | + return Err(anyhow!("'{login}' is not a valid GitHub username")); | |
| 173 | + } | |
| 174 | + let owner = self.token_owner().await?; | |
| 175 | + if let Some(owner) = owner.as_ref().filter(|o| o.login.eq_ignore_ascii_case(login)) { | |
| 176 | + return Ok((owner.clone(), Some(owner.clone()))); | |
| 177 | + } | |
| 178 | + match self.account(login).await { | |
| 179 | + Ok((account, _)) => Ok((account, owner)), | |
| 180 | + Err(error) if error.to_string().starts_with("GitHub has no user") => Err(anyhow!( | |
| 181 | + "GitHub shows no user or organization named '{login}'. If it is your own account and GitHub has \ | |
| 182 | + hidden it, add a token that belongs to it and it can still be imported." | |
| 183 | + )), | |
| 184 | + Err(error) => Err(error), | |
| 185 | + } | |
| 186 | + } | |
| 187 | + | |
| 165 | 188 | /// The account the token belongs to, if there is a token. |
| 166 | 189 | pub async fn token_owner(&self) -> anyhow::Result<Option<RawAccount>> { |
| 167 | 190 | if self.token.is_none() { |
+3-5backend/src/importer/mod.rs
| @@ -82,8 +82,7 @@ pub fn forget_token(key: &str) { | ||
| 82 | 82 | pub async fn preview(source_owner: &str, token: Option<String>) -> AppResult<shared::GithubPreview> { |
| 83 | 83 | let gh = GitHub::new(token)?; |
| 84 | 84 | let login = source_owner.trim().trim_start_matches('@').trim_start_matches("https://github.com/").trim_end_matches('/'); |
| 85 | - let (account, _) = gh.account(login).await.map_err(|e| AppError::bad(e.to_string()))?; | |
| 86 | - let token_owner = gh.token_owner().await.map_err(|e| AppError::bad(e.to_string()))?; | |
| 85 | + let (account, token_owner) = gh.resolve(login).await.map_err(|e| AppError::bad(e.to_string()))?; | |
| 87 | 86 | let token_is_owner = token_owner.as_ref().is_some_and(|o| o.id == account.id); |
| 88 | 87 | let (repos, remaining) = gh.repos(&account, token_is_owner).await.map_err(|e| AppError::bad(e.to_string()))?; |
| 89 | 88 | tracing::info!(login = %account.login, repos = repos.len(), token = gh.has_token(), token_is_owner, "github preview"); |
| @@ -126,8 +125,7 @@ pub async fn start(state: &AppState, viewer: &Viewer, request: shared::StartImpo | ||
| 126 | 125 | }; |
| 127 | 126 | |
| 128 | 127 | let gh = GitHub::new(request.token.clone())?; |
| 129 | - let (account, _) = gh.account(&request.source_owner).await.map_err(|e| AppError::bad(e.to_string()))?; | |
| 130 | - let token_owner = gh.token_owner().await.map_err(|e| AppError::bad(e.to_string()))?; | |
| 128 | + let (account, token_owner) = gh.resolve(&request.source_owner).await.map_err(|e| AppError::bad(e.to_string()))?; | |
| 131 | 129 | let token_is_owner = token_owner.as_ref().is_some_and(|o| o.id == account.id); |
| 132 | 130 | if request.link_emails && !(token_is_owner && target.id == viewer.id) { |
| 133 | 131 | return Err(AppError::bad( |
| @@ -421,7 +419,7 @@ async fn link_emails(state: &AppState, gh: &GitHub, import: &ImportRow) -> anyho | ||
| 421 | 419 | /// Fills empty profile fields (and a missing avatar) from GitHub. Anything |
| 422 | 420 | /// already set on irongit is left alone. |
| 423 | 421 | async fn import_profile(state: &AppState, gh: &GitHub, import: &ImportRow, owner: &Account) -> anyhow::Result<String> { |
| 424 | - let (account, _) = gh.account(&import.source_owner).await?; | |
| 422 | + let (account, _) = gh.resolve(&import.source_owner).await?; | |
| 425 | 423 | let website = account.blog.clone().unwrap_or_default(); |
| 426 | 424 | let website = if website.is_empty() || website.starts_with("http") { website } else { format!("https://{website}") }; |
| 427 | 425 | sqlx::query( |