use crate::DaemonResult as Result;
use serde::{Deserialize, Serialize};
use signer_core::{SignerJWT, SignerJWTClaims, SignerJWTHeader, SignerUser};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthCodeStatus {
Pending,
Fetched,
Expired,
Completed,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuthCodeDetail {
pub code: String,
pub state: String,
pub status: AuthCodeStatus,
pub expire_at: i64,
pub jwt: Option<String>,
}
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct PostAuthRequest {
#[serde(rename = "jwt")]
pub jwt: String,
#[serde(rename = "state")]
pub state: String,
}
impl PostAuthRequest {
pub fn new(jwt: String, state: String) -> PostAuthRequest {
PostAuthRequest { jwt, state }
}
}
pub struct SignerRemoteAuth {
pub base_url: String,
}
impl SignerRemoteAuth {
pub fn from_url(url: String) -> Self {
Self { base_url: url }
}
pub async fn get_auth_detail(&self, target: &str) -> Result<AuthCodeDetail> {
let client = reqwest::Client::new();
let response = client.get(target).send().await?;
if !response.status().is_success() {
return Err(crate::DaemonError::Signer(crate::SignerError::Msg(
format!("获取认证详情失败: {}", response.status()),
)));
}
let detail: AuthCodeDetail = response.json().await?;
Ok(detail)
}
pub async fn post_auth(
&self,
user: &SignerUser,
target: String,
state: String,
expire: i64,
) -> Result<()> {
let claims =
SignerJWTClaims::default(&user, target.clone(), uuid::Uuid::new_v4().to_string())
.with_expired_duration(chrono::Duration::milliseconds(expire));
let jwt = SignerJWT::new(SignerJWTHeader::default(&user), claims)
.encode(&user)
.expect("encode jwt string failed");
let request = PostAuthRequest::new(jwt, state);
let client = reqwest::Client::new();
let response = client.post(&target).json(&request).send().await?;
if !response.status().is_success() {
return Err(crate::DaemonError::Signer(crate::SignerError::Msg(
format!("认证请求失败: {}", response.status()),
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::StreamExt;
use signer_core::SignerUser;
use std::time::Duration;
use tokio::sync::mpsc;
#[tokio::test]
async fn test_e2e_auth_flow() {
let base_url = "http://localhost:8080";
let (tx, mut rx) = mpsc::channel::<String>(1);
let client_handle = tokio::spawn(async move {
let client = reqwest::Client::new();
let mut stream = client
.get(format!("{}/api/auth/new", base_url))
.send()
.await
.unwrap()
.bytes_stream();
let mut code_received = false;
let mut buffer = String::new();
while let Some(item) = stream.next().await {
let chunk = item.unwrap();
let chunk_str = String::from_utf8(chunk.to_vec()).unwrap();
buffer.push_str(&chunk_str);
while let Some(event_end) = buffer.find("\n\n") {
let event = buffer[..event_end].to_string();
buffer = buffer[event_end + 2..].to_string();
println!("Received event: {}", event);
for line in event.lines() {
if line.starts_with("data:") {
let data = line.trim_start_matches("data:").trim();
match serde_json::from_str::<serde_json::Value>(data) {
Ok(auth_code) => {
if !code_received {
if let (Some(code), Some(status)) = (
auth_code.get("code").and_then(|c| c.as_str()),
auth_code.get("status").and_then(|s| s.as_str()),
) {
if status == "Pending" {
println!(
"Received code: {}, status: {}",
code, status
);
tx.send(code.to_string()).await.unwrap();
code_received = true;
}
}
} else {
if let Some(status) =
auth_code.get("status").and_then(|s| s.as_str())
{
if status == "Completed" {
if let Some(jwt) =
auth_code.get("jwt").and_then(|j| j.as_str())
{
println!(
"Received JWT: {}, status: {}",
jwt, status
);
assert!(!jwt.is_empty());
return;
}
}
}
}
}
Err(_) => {
}
}
}
}
}
}
});
let auth_handle = tokio::spawn(async move {
if let Some(code) = rx.recv().await {
let auth = SignerRemoteAuth::from_url(base_url.to_string());
let detail_url = format!("{}/api/auth/{}", base_url, code);
let detail = auth.get_auth_detail(&detail_url).await.unwrap();
println!("Retrieved state from detail API: {}", detail.state);
println!(
"Auth detail: code={}, status={:?}, expire_at={}",
detail.code, detail.status, detail.expire_at
);
let user = SignerUser::generete("tester").unwrap();
let target_url = format!("{}/api/auth/{}", base_url, code);
let result = auth.post_auth(&user, target_url, detail.state, 3600).await;
assert!(result.is_ok());
}
});
let timeout = Duration::from_secs(10);
let _ = tokio::time::timeout(timeout, async {
let (client_res, auth_res) = tokio::join!(client_handle, auth_handle);
client_res.unwrap();
auth_res.unwrap();
})
.await
.expect("测试超时!");
}
}