refactor: workspace secrets-core + secrets-mcp MCP SaaS
- Split library (db/crypto/service) and MCP/Web/OAuth binary - Add deploy examples and CI/docs updates Made-with: Cursor
This commit is contained in:
66
crates/secrets-mcp/src/oauth/google.rs
Normal file
66
crates/secrets-mcp/src/oauth/google.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{OAuthConfig, OAuthUserInfo};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TokenResponse {
|
||||
access_token: String,
|
||||
#[allow(dead_code)]
|
||||
token_type: String,
|
||||
#[allow(dead_code)]
|
||||
id_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UserInfo {
|
||||
sub: String,
|
||||
email: Option<String>,
|
||||
name: Option<String>,
|
||||
picture: Option<String>,
|
||||
}
|
||||
|
||||
/// Exchange authorization code for tokens and fetch user profile.
|
||||
pub async fn exchange_code(
|
||||
client: &reqwest::Client,
|
||||
config: &OAuthConfig,
|
||||
code: &str,
|
||||
) -> Result<OAuthUserInfo> {
|
||||
let token_resp: TokenResponse = client
|
||||
.post("https://oauth2.googleapis.com/token")
|
||||
.form(&[
|
||||
("code", code),
|
||||
("client_id", &config.client_id),
|
||||
("client_secret", &config.client_secret),
|
||||
("redirect_uri", &config.redirect_uri),
|
||||
("grant_type", "authorization_code"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.context("failed to exchange Google code")?
|
||||
.error_for_status()
|
||||
.context("Google token endpoint error")?
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse Google token response")?;
|
||||
|
||||
let user: UserInfo = client
|
||||
.get("https://openidconnect.googleapis.com/v1/userinfo")
|
||||
.bearer_auth(&token_resp.access_token)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to fetch Google userinfo")?
|
||||
.error_for_status()
|
||||
.context("Google userinfo endpoint error")?
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse Google userinfo")?;
|
||||
|
||||
Ok(OAuthUserInfo {
|
||||
provider: "google".to_string(),
|
||||
provider_id: user.sub,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatar_url: user.picture,
|
||||
})
|
||||
}
|
||||
45
crates/secrets-mcp/src/oauth/mod.rs
Normal file
45
crates/secrets-mcp/src/oauth/mod.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
pub mod google;
|
||||
pub mod wechat; // not yet implemented — placeholder for future WeChat integration
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Normalized OAuth user profile from any provider.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OAuthUserInfo {
|
||||
pub provider: String,
|
||||
pub provider_id: String,
|
||||
pub email: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
/// OAuth provider configuration.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct OAuthConfig {
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub redirect_uri: String,
|
||||
}
|
||||
|
||||
/// Build the Google authorization URL.
|
||||
pub fn google_auth_url(config: &OAuthConfig, state: &str) -> String {
|
||||
format!(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth\
|
||||
?client_id={}\
|
||||
&redirect_uri={}\
|
||||
&response_type=code\
|
||||
&scope=openid%20email%20profile\
|
||||
&state={}\
|
||||
&access_type=offline",
|
||||
urlencoding::encode(&config.client_id),
|
||||
urlencoding::encode(&config.redirect_uri),
|
||||
urlencoding::encode(state),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn random_state() -> String {
|
||||
use rand::RngExt;
|
||||
let mut bytes = [0u8; 16];
|
||||
rand::rng().fill(&mut bytes);
|
||||
bytes.iter().map(|b| format!("{:02x}", b)).collect()
|
||||
}
|
||||
18
crates/secrets-mcp/src/oauth/wechat.rs
Normal file
18
crates/secrets-mcp/src/oauth/wechat.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::{OAuthConfig, OAuthUserInfo};
|
||||
/// WeChat OAuth — not yet implemented.
|
||||
///
|
||||
/// This module is a placeholder for future WeChat Open Platform integration.
|
||||
/// When ready, implement `exchange_code` following the non-standard WeChat OAuth 2.0 flow:
|
||||
/// - Token exchange uses a GET request (not POST)
|
||||
/// - Preferred user identifier is `unionid` (cross-app), falling back to `openid`
|
||||
/// - Docs: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn exchange_code(
|
||||
_client: &reqwest::Client,
|
||||
_config: &OAuthConfig,
|
||||
_code: &str,
|
||||
) -> Result<OAuthUserInfo> {
|
||||
bail!("WeChat login is not yet implemented")
|
||||
}
|
||||
Reference in New Issue
Block a user