use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorizationCodeRequest {
pub app_id: String,
pub redirect_uri: String,
pub scope: Option<String>,
pub state: Option<String>,
pub response_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorizationCodeResponse {
pub code: String,
pub state: Option<String>,
}
#[derive(Debug, Clone)]
pub struct AuthorizationUrlBuilder {
app_id: String,
redirect_uri: String,
scope: Option<String>,
state: Option<String>,
response_type: String,
base_url: String,
}
impl AuthorizationUrlBuilder {
pub fn new(app_id: String, redirect_uri: String) -> Self {
Self {
app_id,
redirect_uri,
scope: None,
state: None,
response_type: "code".to_string(),
base_url: "https://open.feishu.cn".to_string(),
}
}
pub fn scope(mut self, scope: impl Into<String>) -> Self {
self.scope = Some(scope.into());
self
}
pub fn state(mut self, state: impl Into<String>) -> Self {
self.state = Some(state.into());
self
}
pub fn response_type(mut self, response_type: impl Into<String>) -> Self {
self.response_type = response_type.into();
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
pub fn build_url(self) -> String {
let mut url = format!(
"{}/open-apis/authen/v1/index?app_id={}&redirect_uri={}",
self.base_url,
urlencoding::encode(&self.app_id),
urlencoding::encode(&self.redirect_uri)
);
if let Some(scope) = &self.scope {
url.push_str(&format!("&scope={}", urlencoding::encode(scope)));
}
if let Some(state) = &self.state {
url.push_str(&format!("&state={}", urlencoding::encode(state)));
}
url.push_str(&format!("&response_type={}", self.response_type));
url
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuthConfig {
pub app_id: String,
pub app_secret: String,
pub redirect_uri: String,
pub scope: Option<String>,
pub base_url: String,
}
impl Default for OAuthConfig {
fn default() -> Self {
Self {
app_id: String::new(),
app_secret: String::new(),
redirect_uri: String::new(),
scope: None,
base_url: "https://open.feishu.cn".to_string(),
}
}
}
impl OAuthConfig {
pub fn new(app_id: String, app_secret: String, redirect_uri: String) -> Self {
Self {
app_id,
app_secret,
redirect_uri,
scope: None,
base_url: "https://open.feishu.cn".to_string(),
}
}
pub fn scope(mut self, scope: impl Into<String>) -> Self {
self.scope = Some(scope.into());
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
pub fn build_authorization_url(&self, state: Option<String>) -> String {
let mut builder =
AuthorizationUrlBuilder::new(self.app_id.clone(), self.redirect_uri.clone());
if let Some(scope) = &self.scope {
builder = builder.scope(scope);
}
if let Some(state) = state {
builder = builder.state(state);
}
builder.base_url(&self.base_url).build_url()
}
}
#[derive(Debug, Clone)]
pub struct AuthorizationState {
pub state: String,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub app_id: String,
pub redirect_uri: String,
}
impl AuthorizationState {
pub fn new(app_id: String, redirect_uri: String) -> Self {
let now = Utc::now();
let expires_at = now + chrono::Duration::minutes(30);
let state = uuid::Uuid::new_v4().to_string();
Self {
state,
created_at: now,
expires_at,
app_id,
redirect_uri,
}
}
pub fn is_expired(&self) -> bool {
Utc::now() >= self.expires_at
}
pub fn is_valid(&self) -> bool {
!self.is_expired() && !self.state.is_empty()
}
pub fn remaining_seconds(&self) -> i64 {
(self.expires_at - Utc::now()).num_seconds().max(0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OAuthError {
InvalidClient,
InvalidScope,
InvalidGrant,
InvalidRedirectUri,
ExpiredGrant,
UsedGrant,
AccessDenied,
ServerError,
TemporarilyUnavailable,
Other(String),
}
impl OAuthError {
pub fn from_string(error: &str) -> Self {
match error {
"invalid_client" => OAuthError::InvalidClient,
"invalid_scope" => OAuthError::InvalidScope,
"invalid_grant" => OAuthError::InvalidGrant,
"invalid_redirect_uri" => OAuthError::InvalidRedirectUri,
"expired_grant" => OAuthError::ExpiredGrant,
"used_grant" => OAuthError::UsedGrant,
"access_denied" => OAuthError::AccessDenied,
"server_error" => OAuthError::ServerError,
"temporarily_unavailable" => OAuthError::TemporarilyUnavailable,
other => OAuthError::Other(other.to_string()),
}
}
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
match self {
OAuthError::InvalidClient => "invalid_client".to_string(),
OAuthError::InvalidScope => "invalid_scope".to_string(),
OAuthError::InvalidGrant => "invalid_grant".to_string(),
OAuthError::InvalidRedirectUri => "invalid_redirect_uri".to_string(),
OAuthError::ExpiredGrant => "expired_grant".to_string(),
OAuthError::UsedGrant => "used_grant".to_string(),
OAuthError::AccessDenied => "access_denied".to_string(),
OAuthError::ServerError => "server_error".to_string(),
OAuthError::TemporarilyUnavailable => "temporarily_unavailable".to_string(),
OAuthError::Other(msg) => msg.clone(),
}
}
pub fn user_friendly_message(&self) -> String {
match self {
OAuthError::InvalidClient => "无效的应用客户端信息".to_string(),
OAuthError::InvalidScope => "无效的授权范围".to_string(),
OAuthError::InvalidGrant => "无效的授权码".to_string(),
OAuthError::InvalidRedirectUri => "无效的重定向地址".to_string(),
OAuthError::ExpiredGrant => "授权码已过期,请重新授权".to_string(),
OAuthError::UsedGrant => "授权码已被使用".to_string(),
OAuthError::AccessDenied => "用户拒绝了授权请求".to_string(),
OAuthError::ServerError => "服务器内部错误,请稍后重试".to_string(),
OAuthError::TemporarilyUnavailable => "服务暂时不可用,请稍后重试".to_string(),
OAuthError::Other(msg) => format!("授权失败: {msg}"),
}
}
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
#[test]
fn test_authorization_url_builder() {
let url = AuthorizationUrlBuilder::new(
"test_app_id".to_string(),
"https://example.com/callback".to_string(),
)
.scope("user_info")
.state("test_state")
.build_url();
assert!(url.contains("app_id=test_app_id"));
assert!(url.contains("redirect_uri=https%3A%2F%2Fexample.com%2Fcallback"));
assert!(url.contains("scope=user_info"));
assert!(url.contains("state=test_state"));
}
#[test]
fn test_oauth_config() {
let config = OAuthConfig::new(
"test_app_id".to_string(),
"test_app_secret".to_string(),
"https://example.com/callback".to_string(),
)
.scope("user_info");
let auth_url = config.build_authorization_url(Some("test_state".to_string()));
assert!(auth_url.contains("app_id=test_app_id"));
assert!(auth_url.contains("scope=user_info"));
assert!(auth_url.contains("state=test_state"));
}
#[test]
fn test_authorization_state() {
let state = AuthorizationState::new(
"test_app_id".to_string(),
"https://example.com/callback".to_string(),
);
assert_eq!(state.app_id, "test_app_id");
assert!(!state.state.is_empty());
assert!(state.is_valid());
assert!(!state.is_expired());
assert!(state.remaining_seconds() > 0);
}
#[test]
fn test_oauth_error() {
let error = OAuthError::InvalidGrant;
assert_eq!(error.to_string(), "invalid_grant");
assert!(!error.user_friendly_message().is_empty());
let error2 = OAuthError::from_string("invalid_grant");
assert_eq!(error.to_string(), error2.to_string());
}
}