Files
zesdex/src/service/oauth/manager.rs
T
asepharyana c1ad206a00 feat: add lesson export and import functionality
- Implemented `LessonExport` and `LessonImport` actions in the action module.
- Added corresponding command parsing for lesson export and import.
- Created functions to handle lesson export and import in the memory module.
- Updated state management to reflect changes after lesson operations.
- Introduced deferred operations for handling asynchronous tasks in the event loop.
- Enhanced the tool execution context to include graduated checks for file operations.
- Added OAuth support with PKCE for secure authorization flows.
- Implemented a loopback server for handling OAuth redirects.
- Refactored various modules to improve code organization and maintainability.
2026-07-11 18:23:01 +07:00

154 lines
5.3 KiB
Rust

use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuthToken {
pub access_token: String,
pub refresh_token: Option<String>,
pub expires_at: u64,
pub token_type: String,
}
impl OAuthToken {
pub fn is_expired(&self) -> bool {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
now >= self.expires_at
}
pub fn remaining_secs(&self) -> i64 {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
self.expires_at as i64 - now as i64
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuthConfig {
pub auth_url: String,
pub token_url: String,
pub client_id: String,
pub client_secret: Option<String>,
pub scopes: Vec<String>,
}
impl Default for OAuthConfig {
fn default() -> Self {
OAuthConfig {
auth_url: String::new(),
token_url: String::new(),
client_id: String::new(),
client_secret: None,
scopes: vec!["openid".to_string(), "profile".to_string(), "email".to_string()],
}
}
}
pub struct OAuthManager {
pub config: OAuthConfig,
pub token: Option<OAuthToken>,
client: reqwest::blocking::Client,
}
impl OAuthManager {
pub fn new(config: OAuthConfig) -> Self {
OAuthManager {
config,
token: None,
client: reqwest::blocking::Client::new(),
}
}
pub fn exchange_code(&mut self, code: &str, redirect_uri: &str, code_verifier: &str) -> Result<(), String> {
let mut params = std::collections::HashMap::new();
params.insert("grant_type", "authorization_code");
params.insert("code", code);
params.insert("redirect_uri", redirect_uri);
params.insert("client_id", &self.config.client_id);
params.insert("code_verifier", code_verifier);
let resp = self.client
.post(&self.config.token_url)
.form(&params)
.send()
.map_err(|e| format!("token request failed: {}", e))?;
let status = resp.status();
let body: serde_json::Value = resp.json().map_err(|e| format!("parse failed: {}", e))?;
if !status.is_success() {
return Err(format!("token endpoint returned {}: {}", status, body));
}
let access_token = body["access_token"].as_str().ok_or("missing access_token")?.to_string();
let expires_in = body["expires_in"].as_u64().unwrap_or(3600);
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
self.token = Some(OAuthToken {
access_token,
refresh_token: body["refresh_token"].as_str().map(|s| s.to_string()),
expires_at: now + expires_in,
token_type: body["token_type"].as_str().unwrap_or("Bearer").to_string(),
});
Ok(())
}
pub fn refresh_token(&mut self) -> Result<(), String> {
let refresh_token = self.token.as_ref()
.and_then(|t| t.refresh_token.clone())
.ok_or("no refresh token available")?;
let mut params = std::collections::HashMap::new();
params.insert("grant_type", "refresh_token");
params.insert("refresh_token", &refresh_token);
params.insert("client_id", &self.config.client_id);
let resp = self.client
.post(&self.config.token_url)
.form(&params)
.send()
.map_err(|e| format!("refresh failed: {}", e))?;
let status = resp.status();
let body: serde_json::Value = resp.json().map_err(|e| format!("parse failed: {}", e))?;
if !status.is_success() {
return Err(format!("refresh endpoint returned {}: {}", status, body));
}
let access_token = body["access_token"].as_str().ok_or("missing access_token")?.to_string();
let expires_in = body["expires_in"].as_u64().unwrap_or(3600);
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
self.token = Some(OAuthToken {
access_token,
refresh_token: body["refresh_token"].as_str().map(|s| s.to_string()).or(self.token.as_ref().and_then(|t| t.refresh_token.clone())),
expires_at: now + expires_in,
token_type: body["token_type"].as_str().unwrap_or("Bearer").to_string(),
});
Ok(())
}
pub fn ensure_token(&mut self) -> Result<(), String> {
if let Some(ref token) = self.token {
if token.remaining_secs() < 60 {
return self.refresh_token();
}
}
Ok(())
}
pub fn build_auth_url(&self, redirect_uri: &str, state: &str, code_challenge: &str) -> String {
let mut url = url::Url::parse(&self.config.auth_url).unwrap_or_else(|_| url::Url::parse("https://example.com").unwrap());
url.query_pairs_mut()
.append_pair("response_type", "code")
.append_pair("client_id", &self.config.client_id)
.append_pair("redirect_uri", redirect_uri)
.append_pair("scope", &self.config.scopes.join(" "))
.append_pair("state", state)
.append_pair("code_challenge_method", "S256")
.append_pair("code_challenge", code_challenge);
url.to_string()
}
}