use std::borrow::Cow;
use anyhow::bail;
use serde::Deserialize;
use super::{provider::OauthUser, HTTP_CLIENT};
pub(super) struct GithubOauthService;
const GITHUB_API_URL: &str = "https://api.github.com";
const GITHUB_OAUTH_URL: &str = "https://github.com/login/oauth/authorize";
const GITHUB_OAUTH_TOKEN_URL: &str = "https://github.com/login/oauth/access_token";
impl GithubOauthService {
pub fn get_oauth_callback_url(client_id: &str, state: &str) -> String {
let redirect_uri = "";
format!("{GITHUB_OAUTH_URL}?client_id={client_id}&redirect_uri={redirect_uri}&state={state}&scope=user:email&response_type=code")
}
pub async fn get_oauth_user(
client_id: &str,
client_secret: &str,
code: &str,
) -> Result<OauthUser, anyhow::Error> {
let token = Self::get_oauth_token(client_id, client_secret, code).await?;
if token.access_token.is_none() {
bail!("Failed to get access token from Github: {:?}", token.error);
}
let access_token = token.access_token.unwrap();
let user = Self::get_user_info(&access_token).await?;
let emails = Self::get_user_emails(&access_token).await?;
let primary_email = emails.iter().find(|e| e.primary).unwrap();
Ok(OauthUser {
email: Cow::Owned(primary_email.email.to_string()),
team_ids: vec![],
organization_ids: vec![],
usernames: vec![user.login.to_string()],
})
}
async fn get_oauth_token(
client_id: &str,
client_secret: &str,
code: &str,
) -> Result<GithubTokenResponse, anyhow::Error> {
let response = HTTP_CLIENT
.post(GITHUB_OAUTH_TOKEN_URL)
.query(&[
("client_id", client_id),
("client_secret", client_secret),
("code", code),
])
.header(http::header::ACCEPT, "application/json")
.send()
.await?;
let body = response.json::<GithubTokenResponse>().await?;
Ok(body)
}
async fn get_user_info(token: &str) -> Result<GithubUserResponse, anyhow::Error> {
tracing::debug!("Getting user info from Github");
let response = HTTP_CLIENT
.get(format!("{GITHUB_API_URL}/user"))
.bearer_auth(token)
.header(http::header::USER_AGENT, "pingora/0.2.0")
.send()
.await?;
let body = response.json::<GithubUserResponse>().await?;
Ok(body)
}
async fn get_user_emails(token: &str) -> Result<Vec<GithubEmailResponse>, anyhow::Error> {
tracing::debug!("Getting user emails from Github");
let response = HTTP_CLIENT
.get(format!("{GITHUB_API_URL}/user/emails"))
.bearer_auth(token)
.header(http::header::USER_AGENT, "pingora/0.2.0")
.send()
.await?;
let body = response.json::<Vec<GithubEmailResponse>>().await?;
Ok(body)
}
}
#[derive(Deserialize, Debug)]
struct GithubTokenResponse {
error: Option<Cow<'static, str>>,
access_token: Option<Cow<'static, str>>,
}
#[derive(Deserialize, Debug)]
struct GithubEmailResponse {
email: Cow<'static, str>,
primary: bool,
}
#[derive(Deserialize, Debug)]
struct GithubUserResponse {
login: Cow<'static, str>,
}