/
codenik
/
codenik-tools
Обзор
Документация
Войти
/
codenik
/
codenik-tools
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
main
crates/codenik-cli/src/auth.rs
173 строки
6 KB
Codenik Wizard
feat(cli): wire auth login/logout/status with keyring and OAuth flow
09 май 2026, 00:56
09 май 2026, 00:56
e999ce9
Код
Авторство
О чём код?
//! Команды `codenik auth login/logout/status`. use anyhow::{Result, anyhow}; use codenik_storage::{CredentialStore, KeyringStore}; use codenik_yandex_tracker::YandexOAuth; use secrecy::SecretString; const PROVIDERS: &[(&str, &str, &str, &str)] = &[ // (display, cli_name, keyring_key, env_var_hint) ("GitVerse", "gitverse", "gitverse.token", "GITVERSE_TOKEN"), ( "SourceCraft", "sourcecraft", "sourcecraft.token", "SOURCECRAFT_TOKEN", ), ( "Yandex Tracker", "yandex-tracker", "yandex_tracker.token", "YANDEX_OAUTH_TOKEN", ), ]; fn provider_meta(name: &str) -> Result<&'static (&'static str, &'static str, &'static str, &'static str)> { PROVIDERS .iter() .find(|p| p.1 == name) .ok_or_else(|| { anyhow!( "неизвестный provider `{name}`. Доступные: gitverse, sourcecraft, yandex-tracker" ) }) } pub async fn login(provider: &str) -> Result<()> { let (display, cli_name, keyring_key, _env_hint) = *provider_meta(provider)?; let store = KeyringStore::new(); match cli_name { "gitverse" | "sourcecraft" => login_pat(&store, display, keyring_key)?, "yandex-tracker" => yandex_tracker_login(&store, keyring_key).await?, _ => unreachable!(), } Ok(()) } pub fn logout(provider: &str) -> Result<()> { let (display, _cli_name, keyring_key, _env_hint) = *provider_meta(provider)?; let store = KeyringStore::new(); store.delete(keyring_key)?; if provider == "yandex-tracker" { let _ = store.delete("yandex_tracker.refresh"); } println!("✓ {display}: токен удалён из keyring"); Ok(()) } pub fn status() -> Result<()> { let keyring = KeyringStore::new(); for (display, _cli_name, keyring_key, env_var) in PROVIDERS { let env_set = std::env::var(env_var) .map(|v| !v.is_empty()) .unwrap_or(false); let kr_set = keyring.get(keyring_key).map(|v| v.is_some()).unwrap_or(false); let (mark, info) = match (env_set, kr_set) { (true, true) => ("✓", format!("env (`{env_var}`) + keyring (`{keyring_key}`)")), (true, false) => ("✓", format!("env (`{env_var}`)")), (false, true) => ("✓", format!("keyring (`{keyring_key}`)")), (false, false) => ("✗", "не настроен".to_string()), }; println!("{mark} {display}: {info}"); } Ok(()) } fn login_pat(store: &dyn CredentialStore, display: &str, keyring_key: &str) -> Result<()> { eprintln!("Введите Personal Access Token для {display} (ввод не отображается):"); let pat = rpassword::read_password()?; let pat = pat.trim().to_string(); if pat.is_empty() { return Err(anyhow!("PAT не введён")); } store.set(keyring_key, SecretString::from(pat))?; println!("✓ {display}: токен сохранён в keyring (`{keyring_key}`)"); Ok(()) } async fn yandex_tracker_login(store: &dyn CredentialStore, keyring_key: &str) -> Result<()> { let client_id = required_env("YANDEX_OAUTH_CLIENT_ID")?; let client_secret = required_env("YANDEX_OAUTH_CLIENT_SECRET")?; let redirect_uri = std::env::var("YANDEX_OAUTH_REDIRECT_URI") .unwrap_or_else(|_| "https://oauth.yandex.ru/verification_code".to_string()); let oauth = YandexOAuth::with_base_url( "https://oauth.yandex.ru", client_id, SecretString::from(client_secret), redirect_uri, ); eprintln!("Откройте в браузере и авторизуйтесь:"); eprintln!(" {}", oauth.authorize_url()); eprintln!(); eprint!("Введите verification code: "); use std::io::Write; std::io::stderr().flush()?; let mut code = String::new(); std::io::stdin().read_line(&mut code)?; let code = code.trim(); if code.is_empty() { return Err(anyhow!("verification code не введён")); } let token = oauth .exchange_code(code) .await .map_err(|e| anyhow!("обмен кода на токен: {e}"))?; store.set(keyring_key, SecretString::from(token.access_token.clone()))?; if let Some(rt) = token.refresh_token { store.set("yandex_tracker.refresh", SecretString::from(rt))?; } println!( "✓ Yandex Tracker: access token сохранён в keyring (`{keyring_key}`), \ действителен ~{days} дней", days = token.expires_in / 86_400 ); Ok(()) } fn required_env(name: &str) -> Result<String> { std::env::var(name) .ok() .filter(|v| !v.is_empty()) .ok_or_else(|| { anyhow!( "не задана переменная окружения `{name}`. \ Зарегистрируйте OAuth-приложение на oauth.yandex.ru \ и установите YANDEX_OAUTH_CLIENT_ID, YANDEX_OAUTH_CLIENT_SECRET." ) }) } #[cfg(test)] mod tests { use super::*; #[test] fn provider_meta_known() { let (display, _, kr, _) = *provider_meta("gitverse").unwrap(); assert_eq!(display, "GitVerse"); assert_eq!(kr, "gitverse.token"); } #[test] fn provider_meta_unknown_errors() { let err = provider_meta("nope").unwrap_err(); assert!(err.to_string().contains("неизвестный")); } #[test] fn required_env_missing_reports_var_name() { // SAFETY: tests run sequentially in scope. unsafe { std::env::remove_var("CODENIK_TEST_MISSING_ENV_VAR"); } let err = required_env("CODENIK_TEST_MISSING_ENV_VAR").unwrap_err(); assert!(err.to_string().contains("CODENIK_TEST_MISSING_ENV_VAR")); } }