mirror of
https://codeberg.org/Cyborus/forgejo-cli.git
synced 2024-11-27 12:03:49 +01:00
Merge pull request 'add fj auth login
' (#71) from oauth-login into main
Reviewed-on: https://codeberg.org/Cyborus/forgejo-cli/pulls/71
This commit is contained in:
commit
9725bb6026
10 changed files with 496 additions and 57 deletions
|
@ -7,6 +7,7 @@ steps:
|
|||
- rustup target add x86_64-unknown-linux-gnu
|
||||
- cargo build --target=x86_64-unknown-linux-gnu --release
|
||||
- strip target/x86_64-unknown-linux-gnu/release/fj
|
||||
secrets: [ client_info_codeberg ]
|
||||
compile-windows:
|
||||
image: rust:latest
|
||||
commands:
|
||||
|
@ -15,6 +16,7 @@ steps:
|
|||
- apt install gcc-mingw-w64-x86-64 -y
|
||||
- cargo build --target=x86_64-pc-windows-gnu --release
|
||||
- strip target/x86_64-pc-windows-gnu/release/fj.exe
|
||||
secrets: [ client_info_codeberg ]
|
||||
zip:
|
||||
image: debian:12
|
||||
commands:
|
||||
|
|
241
Cargo.lock
generated
241
Cargo.lock
generated
|
@ -65,6 +65,23 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "auth-git2"
|
||||
version = "0.5.4"
|
||||
|
@ -121,6 +138,15 @@ version = "2.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
|
@ -212,6 +238,25 @@ version = "0.8.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
|
@ -222,6 +267,16 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "5.0.1"
|
||||
|
@ -298,15 +353,20 @@ name = "fj"
|
|||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"auth-git2",
|
||||
"base64ct",
|
||||
"clap",
|
||||
"directories",
|
||||
"eyre",
|
||||
"forgejo-api",
|
||||
"futures",
|
||||
"git2",
|
||||
"hyper 1.3.1",
|
||||
"hyper-util",
|
||||
"open",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha256",
|
||||
"soft_assert",
|
||||
"time",
|
||||
"tokio",
|
||||
|
@ -337,9 +397,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
|||
|
||||
[[package]]
|
||||
name = "forgejo-api"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0452961a1435fdf127da1a1ba13cb840efe26bf6169bfd8d6dafecdd3e0eafa"
|
||||
checksum = "c9938546bc436425957b6365e204972f5e5a8c0318aed9e7e0f60a5eb5630f0c"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"bytes",
|
||||
|
@ -452,6 +512,16 @@ dependencies = [
|
|||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.14"
|
||||
|
@ -495,7 +565,26 @@ dependencies = [
|
|||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http 0.2.12",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.1.0",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
|
@ -521,6 +610,12 @@ version = "0.3.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
|
@ -532,6 +627,17 @@ dependencies = [
|
|||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
|
@ -539,10 +645,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
|
@ -565,9 +681,9 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
|
@ -579,6 +695,26 @@ dependencies = [
|
|||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2 0.4.5",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
|
@ -586,12 +722,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"hyper 0.14.28",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"hyper 1.3.1",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
|
@ -962,6 +1113,12 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.81"
|
||||
|
@ -980,6 +1137,36 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.1"
|
||||
|
@ -1011,10 +1198,10 @@ dependencies = [
|
|||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.28",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
|
@ -1156,6 +1343,30 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha256"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"hex",
|
||||
"sha2",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
|
@ -1413,6 +1624,12 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
|
|
|
@ -7,15 +7,20 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
auth-git2 = "0.5.3"
|
||||
base64ct = { version = "1.6.0", features = ["std"] }
|
||||
clap = { version = "4.3.11", features = ["derive"] }
|
||||
directories = "5.0.1"
|
||||
eyre = "0.6.8"
|
||||
forgejo-api = "0.2.0"
|
||||
forgejo-api = "0.3.0"
|
||||
futures = "0.3.28"
|
||||
git2 = "0.17.2"
|
||||
hyper = "1.3.1"
|
||||
hyper-util = { version = "0.1.5", features = ["tokio", "server", "http1", "http2"] }
|
||||
open = "5.0.0"
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0.170", features = ["derive"] }
|
||||
serde_json = "1.0.100"
|
||||
sha256 = "1.5.0"
|
||||
soft_assert = "0.1.1"
|
||||
time = { version = "0.3.30", features = ["formatting", "macros"] }
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
|
|
184
src/auth.rs
184
src/auth.rs
|
@ -1,4 +1,5 @@
|
|||
use clap::Subcommand;
|
||||
use eyre::OptionExt;
|
||||
|
||||
#[derive(Subcommand, Clone, Debug)]
|
||||
pub enum AuthCommand {
|
||||
|
@ -18,12 +19,28 @@ pub enum AuthCommand {
|
|||
}
|
||||
|
||||
impl AuthCommand {
|
||||
pub async fn run(self, keys: &mut crate::KeyInfo) -> eyre::Result<()> {
|
||||
pub async fn run(self, keys: &mut crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
|
||||
match self {
|
||||
AuthCommand::Login => {
|
||||
todo!();
|
||||
// let user = readline("username: ").await?;
|
||||
// let pass = readline("password: ").await?;
|
||||
let repo_info = crate::repo::RepoInfo::get_current(host_name, None, None)?;
|
||||
let host_url = repo_info.host_url();
|
||||
let client_info = get_client_info_for(host_url);
|
||||
if let Some((client_id, _)) = client_info {
|
||||
oauth_login(keys, host_url, client_id).await?;
|
||||
} else {
|
||||
let host_domain = host_url.host_str().ok_or_eyre("invalid host")?;
|
||||
let host_path = host_url.path();
|
||||
let mut applications_url = host_url.clone();
|
||||
applications_url
|
||||
.path_segments_mut()
|
||||
.map_err(|_| eyre::eyre!("invalid url"))?
|
||||
.extend(["user", "settings", "applications"]);
|
||||
|
||||
println!("{host_domain}{host_path} doesn't support easy login");
|
||||
println!();
|
||||
println!("Please visit {applications_url}");
|
||||
println!("to create a token, and use it to log in with `fj auth add-token`");
|
||||
}
|
||||
}
|
||||
AuthCommand::Logout { host } => {
|
||||
let info_opt = keys.hosts.remove(&host);
|
||||
|
@ -39,8 +56,13 @@ impl AuthCommand {
|
|||
None => crate::readline("new key: ").await?.trim().to_string(),
|
||||
};
|
||||
if keys.hosts.get(&user).is_none() {
|
||||
keys.hosts
|
||||
.insert(host, crate::keys::LoginInfo::new(user, key));
|
||||
keys.hosts.insert(
|
||||
host,
|
||||
crate::keys::LoginInfo::Application {
|
||||
name: user,
|
||||
token: key,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
println!("key for {} already exists", host);
|
||||
}
|
||||
|
@ -57,3 +79,153 @@ impl AuthCommand {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_client_info_for(url: &url::Url) -> Option<(&'static str, &'static str)> {
|
||||
let client_info = match (url.host_str()?, url.path()) {
|
||||
("codeberg.org", "/") => option_env!("CLIENT_INFO_CODEBERG"),
|
||||
_ => None,
|
||||
};
|
||||
client_info.and_then(|info| info.split_once(":"))
|
||||
}
|
||||
|
||||
async fn oauth_login(
|
||||
keys: &mut crate::KeyInfo,
|
||||
host: &url::Url,
|
||||
client_id: &'static str,
|
||||
) -> eyre::Result<()> {
|
||||
use base64ct::Encoding;
|
||||
use rand::{distributions::Alphanumeric, prelude::*};
|
||||
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let state = (0..32)
|
||||
.map(|_| rng.sample(Alphanumeric) as char)
|
||||
.collect::<String>();
|
||||
let code_verifier = (0..43)
|
||||
.map(|_| rng.sample(Alphanumeric) as char)
|
||||
.collect::<String>();
|
||||
let code_challenge =
|
||||
base64ct::Base64Url::encode_string(sha256::digest(&code_verifier).as_bytes());
|
||||
|
||||
let mut auth_url = host.clone();
|
||||
auth_url
|
||||
.path_segments_mut()
|
||||
.map_err(|_| eyre::eyre!("invalid url"))?
|
||||
.extend(["login", "oauth", "authorize"]);
|
||||
auth_url.query_pairs_mut().extend_pairs([
|
||||
("client_id", client_id),
|
||||
("redirect_uri", "http://127.0.0.1:26218/"),
|
||||
("response_type", "code"),
|
||||
("code_challenge_method", "S256"),
|
||||
("code_challenge", &code_challenge),
|
||||
("state", &state),
|
||||
]);
|
||||
open::that(auth_url.as_str()).unwrap();
|
||||
|
||||
let (handle, mut rx) = auth_server();
|
||||
let res = rx.recv().await.unwrap();
|
||||
handle.abort();
|
||||
let code = match res {
|
||||
Ok(Some((code, returned_state))) => {
|
||||
if returned_state == state {
|
||||
code
|
||||
} else {
|
||||
eyre::bail!("returned with invalid state");
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("Login canceled");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
eyre::bail!("Failed to authenticate: {e}");
|
||||
}
|
||||
};
|
||||
|
||||
let api = forgejo_api::Forgejo::new(forgejo_api::Auth::None, host.clone())?;
|
||||
let request = forgejo_api::structs::OAuthTokenRequest::Public {
|
||||
client_id,
|
||||
code_verifier: &code_verifier,
|
||||
code: &code,
|
||||
redirect_uri: url::Url::parse("http://127.0.0.1:26218/").unwrap(),
|
||||
};
|
||||
let response = api.oauth_get_access_token(request).await?;
|
||||
|
||||
let api = forgejo_api::Forgejo::new(
|
||||
forgejo_api::Auth::OAuth2(&response.access_token),
|
||||
host.clone(),
|
||||
)?;
|
||||
let current_user = api.user_get_current().await?;
|
||||
let name = current_user
|
||||
.login
|
||||
.ok_or_eyre("user does not have login name")?;
|
||||
|
||||
// A minute less, in case any weirdness happens at the exact moment it
|
||||
// expires. Better to refresh slightly too soon than slightly too late.
|
||||
let expires_in = std::time::Duration::from_secs(response.expires_in.saturating_sub(60) as u64);
|
||||
let expires_at = time::OffsetDateTime::now_utc() + expires_in;
|
||||
let login_info = crate::keys::LoginInfo::OAuth {
|
||||
name,
|
||||
token: response.access_token,
|
||||
refresh_token: response.refresh_token,
|
||||
expires_at,
|
||||
};
|
||||
keys.hosts
|
||||
.insert(host.host_str().unwrap().to_string(), login_info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use tokio::{sync::mpsc::Receiver, task::JoinHandle};
|
||||
|
||||
fn auth_server() -> (
|
||||
JoinHandle<eyre::Result<()>>,
|
||||
Receiver<Result<Option<(String, String)>, String>>,
|
||||
) {
|
||||
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 26218).into();
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(1);
|
||||
let tx = std::sync::Arc::new(tx);
|
||||
let handle = tokio::spawn(async move {
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
let server =
|
||||
hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new());
|
||||
let svc = hyper::service::service_fn(|req: hyper::Request<hyper::body::Incoming>| {
|
||||
let tx = std::sync::Arc::clone(&tx);
|
||||
async move {
|
||||
let mut code = None;
|
||||
let mut state = None;
|
||||
let mut error_description = None;
|
||||
if let Some(query) = req.uri().query() {
|
||||
for item in query.split("&") {
|
||||
let (key, value) = item.split_once("=").unwrap_or((item, ""));
|
||||
match key {
|
||||
"code" => code = Some(value),
|
||||
"state" => state = Some(value),
|
||||
"error_description" => error_description = Some(value),
|
||||
_ => eprintln!("unknown key {key} {value}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
let (response, message) = match (code, state, error_description) {
|
||||
(_, _, Some(error)) => (Err(error.to_owned()), "Failed to authenticate"),
|
||||
(Some(code), Some(state), None) => (
|
||||
Ok(Some((code.to_owned(), state.to_owned()))),
|
||||
"Authenticated! Close this tab and head back to your terminal",
|
||||
),
|
||||
_ => (Ok(None), "Canceled"),
|
||||
};
|
||||
tx.send(response).await.unwrap();
|
||||
Ok::<_, hyper::Error>(hyper::Response::new(message.to_owned()))
|
||||
}
|
||||
});
|
||||
loop {
|
||||
let (connection, _addr) = listener.accept().await.unwrap();
|
||||
server
|
||||
.serve_connection(hyper_util::rt::TokioIo::new(connection), svc)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
(handle, rx)
|
||||
}
|
||||
|
|
|
@ -121,10 +121,10 @@ pub enum ViewCommand {
|
|||
}
|
||||
|
||||
impl IssueCommand {
|
||||
pub async fn run(self, keys: &crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
|
||||
pub async fn run(self, keys: &mut crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
|
||||
use IssueSubcommand::*;
|
||||
let repo = RepoInfo::get_current(host_name, self.repo(), self.remote.as_deref())?;
|
||||
let api = keys.get_api(repo.host_url())?;
|
||||
let api = keys.get_api(repo.host_url()).await?;
|
||||
let repo = repo.name().ok_or_else(|| self.no_repo_error())?;
|
||||
match self.command {
|
||||
Create {
|
||||
|
|
74
src/keys.rs
74
src/keys.rs
|
@ -42,7 +42,7 @@ impl KeyInfo {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_login(&self, url: &Url) -> eyre::Result<&LoginInfo> {
|
||||
pub fn get_login(&mut self, url: &Url) -> eyre::Result<&mut LoginInfo> {
|
||||
let host_str = url
|
||||
.host_str()
|
||||
.ok_or_else(|| eyre!("remote url does not have host"))?;
|
||||
|
@ -54,32 +54,76 @@ impl KeyInfo {
|
|||
|
||||
let login_info = self
|
||||
.hosts
|
||||
.get(&domain)
|
||||
.get_mut(&domain)
|
||||
.ok_or_else(|| eyre!("not signed in to {domain}"))?;
|
||||
Ok(login_info)
|
||||
}
|
||||
|
||||
pub fn get_api(&self, url: &Url) -> eyre::Result<forgejo_api::Forgejo> {
|
||||
self.get_login(url)?.api_for(url).map_err(Into::into)
|
||||
pub async fn get_api(&mut self, url: &Url) -> eyre::Result<forgejo_api::Forgejo> {
|
||||
self.get_login(url)?.api_for(url).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
|
||||
pub struct LoginInfo {
|
||||
name: String,
|
||||
key: String,
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum LoginInfo {
|
||||
Application {
|
||||
name: String,
|
||||
token: String,
|
||||
},
|
||||
OAuth {
|
||||
name: String,
|
||||
token: String,
|
||||
refresh_token: String,
|
||||
expires_at: time::OffsetDateTime,
|
||||
},
|
||||
}
|
||||
|
||||
impl LoginInfo {
|
||||
pub fn new(name: String, key: String) -> Self {
|
||||
Self { name, key }
|
||||
}
|
||||
|
||||
pub fn username(&self) -> &str {
|
||||
&self.name
|
||||
match self {
|
||||
LoginInfo::Application { name, .. } => name,
|
||||
LoginInfo::OAuth { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn api_for(&self, url: &Url) -> Result<forgejo_api::Forgejo, forgejo_api::ForgejoError> {
|
||||
forgejo_api::Forgejo::new(forgejo_api::Auth::Token(&self.key), url.clone())
|
||||
pub async fn api_for(&mut self, url: &Url) -> eyre::Result<forgejo_api::Forgejo> {
|
||||
match self {
|
||||
LoginInfo::Application { token, .. } => {
|
||||
let api = forgejo_api::Forgejo::new(forgejo_api::Auth::Token(token), url.clone())?;
|
||||
Ok(api)
|
||||
}
|
||||
LoginInfo::OAuth {
|
||||
token,
|
||||
refresh_token,
|
||||
expires_at,
|
||||
..
|
||||
} => {
|
||||
if time::OffsetDateTime::now_utc() >= *expires_at {
|
||||
let api = forgejo_api::Forgejo::new(forgejo_api::Auth::None, url.clone())?;
|
||||
let (client_id, client_secret) = crate::auth::get_client_info_for(url)
|
||||
.ok_or_else(|| {
|
||||
eyre::eyre!("Can't refresh token; no client info for {url}. How did this happen?")
|
||||
})?;
|
||||
let response = api
|
||||
.oauth_get_access_token(forgejo_api::structs::OAuthTokenRequest::Refresh {
|
||||
refresh_token,
|
||||
client_id,
|
||||
client_secret,
|
||||
})
|
||||
.await?;
|
||||
*token = response.access_token;
|
||||
*refresh_token = response.refresh_token;
|
||||
// A minute less, in case any weirdness happens at the exact moment it
|
||||
// expires. Better to refresh slightly too soon than slightly too late.
|
||||
let expires_in = std::time::Duration::from_secs(
|
||||
response.expires_in.saturating_sub(60) as u64,
|
||||
);
|
||||
*expires_at = time::OffsetDateTime::now_utc() + expires_in;
|
||||
}
|
||||
let api = forgejo_api::Forgejo::new(forgejo_api::Auth::Token(token), url.clone())?;
|
||||
Ok(api)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,9 +50,9 @@ async fn main() -> eyre::Result<()> {
|
|||
let host_name = args.host.as_deref();
|
||||
// let remote = repo::RepoInfo::get_current(host_name, remote_name)?;
|
||||
match args.command {
|
||||
Command::Repo(subcommand) => subcommand.run(&keys, host_name).await?,
|
||||
Command::Issue(subcommand) => subcommand.run(&keys, host_name).await?,
|
||||
Command::Pr(subcommand) => subcommand.run(&keys, host_name).await?,
|
||||
Command::Repo(subcommand) => subcommand.run(&mut keys, host_name).await?,
|
||||
Command::Issue(subcommand) => subcommand.run(&mut keys, host_name).await?,
|
||||
Command::Pr(subcommand) => subcommand.run(&mut keys, host_name).await?,
|
||||
Command::WhoAmI { remote } => {
|
||||
let url = repo::RepoInfo::get_current(host_name, None, remote.as_deref())
|
||||
.wrap_err("could not find host, try specifying with --host")?
|
||||
|
@ -68,7 +68,7 @@ async fn main() -> eyre::Result<()> {
|
|||
println!("currently signed in to {name}@{host}{}", url.path());
|
||||
}
|
||||
}
|
||||
Command::Auth(subcommand) => subcommand.run(&mut keys).await?,
|
||||
Command::Auth(subcommand) => subcommand.run(&mut keys, host_name).await?,
|
||||
Command::Release(subcommand) => subcommand.run(&mut keys, host_name).await?,
|
||||
}
|
||||
|
||||
|
|
17
src/prs.rs
17
src/prs.rs
|
@ -5,7 +5,7 @@ use eyre::OptionExt;
|
|||
use forgejo_api::{
|
||||
structs::{
|
||||
CreatePullRequestOption, MergePullRequestOption, RepoGetPullRequestCommitsQuery,
|
||||
RepoGetPullRequestFilesQuery,
|
||||
RepoGetPullRequestFilesQuery, StateType,
|
||||
},
|
||||
Forgejo,
|
||||
};
|
||||
|
@ -249,10 +249,10 @@ pub enum ViewCommand {
|
|||
}
|
||||
|
||||
impl PrCommand {
|
||||
pub async fn run(self, keys: &crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
|
||||
pub async fn run(self, keys: &mut crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
|
||||
use PrSubcommand::*;
|
||||
let repo = RepoInfo::get_current(host_name, self.repo(), self.remote.as_deref())?;
|
||||
let api = keys.get_api(repo.host_url())?;
|
||||
let api = keys.get_api(repo.host_url()).await?;
|
||||
let repo = repo.name().ok_or_else(|| self.no_repo_error())?;
|
||||
match self.command {
|
||||
Create {
|
||||
|
@ -429,14 +429,13 @@ pub async fn view_pr(repo: &RepoName, api: &Forgejo, id: u64) -> eyre::Result<()
|
|||
};
|
||||
let state = pr
|
||||
.state
|
||||
.as_deref()
|
||||
.ok_or_else(|| eyre::eyre!("pr does not have state"))?;
|
||||
let is_merged = pr.merged.unwrap_or_default();
|
||||
let state = match state {
|
||||
"open" if is_draft => format!("{light_grey}Draft{reset}"),
|
||||
"open" => format!("{bright_green}Open{reset}"),
|
||||
"closed" if pr.merged.unwrap_or_default() => format!("{bright_magenta}Merged{reset}"),
|
||||
"closed" => format!("{bright_red}Closed{reset}"),
|
||||
_ => "Unknown".to_owned(),
|
||||
StateType::Open if is_draft => format!("{light_grey}Draft{reset}"),
|
||||
StateType::Open => format!("{bright_green}Open{reset}"),
|
||||
StateType::Closed if is_merged => format!("{bright_magenta}Merged{reset}"),
|
||||
StateType::Closed => format!("{bright_red}Closed{reset}"),
|
||||
};
|
||||
let base = pr.base.as_ref().ok_or_eyre("pr does not have base")?;
|
||||
let base_repo = base
|
||||
|
|
|
@ -116,10 +116,10 @@ pub enum AssetCommand {
|
|||
}
|
||||
|
||||
impl ReleaseCommand {
|
||||
pub async fn run(self, keys: &KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> {
|
||||
pub async fn run(self, keys: &mut KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> {
|
||||
let repo =
|
||||
RepoInfo::get_current(remote_name, self.repo.as_deref(), self.remote.as_deref())?;
|
||||
let api = keys.get_api(&repo.host_url())?;
|
||||
let api = keys.get_api(&repo.host_url()).await?;
|
||||
let repo = repo
|
||||
.name()
|
||||
.ok_or_eyre("couldn't get repo name, try specifying with --repo")?;
|
||||
|
|
12
src/repo.rs
12
src/repo.rs
|
@ -268,7 +268,7 @@ pub enum RepoCommand {
|
|||
}
|
||||
|
||||
impl RepoCommand {
|
||||
pub async fn run(self, keys: &crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
|
||||
pub async fn run(self, keys: &mut crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
|
||||
match self {
|
||||
RepoCommand::Create {
|
||||
repo,
|
||||
|
@ -287,7 +287,7 @@ impl RepoCommand {
|
|||
}
|
||||
}
|
||||
let host = RepoInfo::get_current(host_name, None, None)?;
|
||||
let api = keys.get_api(host.host_url())?;
|
||||
let api = keys.get_api(host.host_url()).await?;
|
||||
let repo_spec = CreateRepoOption {
|
||||
auto_init: Some(false),
|
||||
default_branch: Some("main".into()),
|
||||
|
@ -343,7 +343,7 @@ impl RepoCommand {
|
|||
}
|
||||
RepoCommand::View { name, remote } => {
|
||||
let repo = RepoInfo::get_current(host_name, name.as_deref(), remote.as_deref())?;
|
||||
let api = keys.get_api(&repo.host_url())?;
|
||||
let api = keys.get_api(&repo.host_url()).await?;
|
||||
let repo = repo
|
||||
.name()
|
||||
.ok_or_eyre("couldn't get repo name, please specify")?;
|
||||
|
@ -454,7 +454,7 @@ impl RepoCommand {
|
|||
}
|
||||
RepoCommand::Clone { repo, path } => {
|
||||
let repo = RepoInfo::get_current(host_name, Some(&repo), None)?;
|
||||
let api = keys.get_api(&repo.host_url())?;
|
||||
let api = keys.get_api(&repo.host_url()).await?;
|
||||
let name = repo.name().unwrap();
|
||||
|
||||
let repo_data = api.repo_get(name.owner(), name.name()).await?;
|
||||
|
@ -544,14 +544,14 @@ impl RepoCommand {
|
|||
}
|
||||
RepoCommand::Star { repo } => {
|
||||
let repo = RepoInfo::get_current(host_name, Some(&repo), None)?;
|
||||
let api = keys.get_api(&repo.host_url())?;
|
||||
let api = keys.get_api(&repo.host_url()).await?;
|
||||
let name = repo.name().unwrap();
|
||||
api.user_current_put_star(name.owner(), name.name()).await?;
|
||||
println!("Starred {}/{}!", name.owner(), name.name());
|
||||
}
|
||||
RepoCommand::Unstar { repo } => {
|
||||
let repo = RepoInfo::get_current(host_name, Some(&repo), None)?;
|
||||
let api = keys.get_api(&repo.host_url())?;
|
||||
let api = keys.get_api(&repo.host_url()).await?;
|
||||
let name = repo.name().unwrap();
|
||||
api.user_current_delete_star(name.owner(), name.name())
|
||||
.await?;
|
||||
|
|
Loading…
Reference in a new issue