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, } #[derive(Deserialize)] struct UserInfo { sub: String, email: Option, name: Option, picture: Option, } /// Exchange authorization code for tokens and fetch user profile. pub async fn exchange_code( client: &reqwest::Client, config: &OAuthConfig, code: &str, ) -> Result { 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, }) }