mod model;
use base64::engine::general_purpose::NO_PAD;
use base64::engine::GeneralPurpose;
use base64::{alphabet, Engine as _};
pub use model::*;
use lazy_static::lazy_static;
use query_map::QueryMap;
use rand::{rng, Rng as _};
use rdkafka::message::ToBytes;
use serde_json::json;
use crate::core::auth0::user_session::TypedSession;
use crate::core::auth0::UserId;
use crate::core::http_clients::HttpClient;
use crate::prelude2::*;
use crate::services::user_service::{save_user_by_github, save_user_by_google};
use crate::utils;
#[rustfmt::skip]
lazy_static! {
static ref GOOGLE_CLIENT_ID: String = crate::commons::read_env("GOOGLE_CLIENT_ID", "");
static ref GOOGLE_CLIENT_SECRET: String = crate::commons::read_env("GOOGLE_CLIENT_SECRET", "");
static ref GOOGLE_CALLBACK_URL: String = crate::commons::read_env("GOOGLE_CALLBACK_URL", "");
static ref GOOGLE_OAUTH_URL: &'static str = "https://accounts.google.com/o/oauth2/v2/auth";
static ref GOOGLE_OAUTH_SCOPES: &'static [&'static str] = &["https%3A//www.googleapis.com/auth/userinfo.email", "https%3A//www.googleapis.com/auth/userinfo.profile"];
static ref GOOGLE_TOKEN_INFO_URL: &'static str = "https://oauth2.googleapis.com/tokeninfo";
static ref GOOGLE_ACCESS_TOKEN_URL: &'static str = "https://oauth2.googleapis.com/token";
static ref GITHUB_CLIENT_ID: String = crate::commons::read_env("GITHUB_CLIENT_ID", "");
static ref GITHUB_CLIENT_SECRET: String = crate::commons::read_env("GITHUB_CLIENT_SECRET", "");
static ref GITHUB_CALLBACK_URL: String = crate::commons::read_env("GITHUB_CALLBACK_URL", "");
static ref GITHUB_OAUTH_URL: &'static str = "https://github.com/login/oauth/authorize";
static ref GITHUB_OAUTH_SCOPES: &'static [&'static str] = &["public_repo", "user:email"];
static ref GITHUB_TOKEN_INFO_URL: &'static str = "https://api.github.com/user";
static ref GITHUB_ACCESS_TOKEN_URL: &'static str = "https://github.com/login/oauth/access_token";
}
pub async fn gl(_request: HttpRequest) -> impl Responder {
let state = "some_state";
#[allow(non_snake_case)]
let scopes = GOOGLE_OAUTH_SCOPES.join(" ");
let cb_url = GOOGLE_CALLBACK_URL.replace(':', "%3A");
#[allow(non_snake_case)]
let GOOGLE_OAUTH_CONSENT_SCREEN_URL = format!(
"{}?client_id={}&redirect_uri={}&access_type=offline&response_type=code&state={}&scope={}",
*GOOGLE_OAUTH_URL, *GOOGLE_CLIENT_ID, cb_url, state, scopes
);
utils::send_redirect(&GOOGLE_OAUTH_CONSENT_SCREEN_URL)
}
pub async fn gl_cb(
context: web::Data<AppContext>,
session: TypedSession,
request: HttpRequest,
) -> impl Responder {
let query_string = if let Some(s) = request.uri().query() {
s.to_string()
} else {
"".to_string()
};
let _map = query_string.parse::<QueryMap>().unwrap();
let mut data = json!({
"client_id": GOOGLE_CLIENT_ID.clone(),
"client_secret": GOOGLE_CLIENT_SECRET.clone(),
"redirect_uri": GOOGLE_CALLBACK_URL.clone(),
"grant_type": "authorization_code",
});
for l in _map.iter() {
if l.0 == "code" {
data[l.0.to_owned()] = json!(l.1.to_owned());
}
}
let http_client = HttpClient::build(5);
let access_token_data = http_client
.post_with_json_data_string(*GOOGLE_ACCESS_TOKEN_URL, &data)
.await
.map_err(|_e| Error::invalid_request("InvalidRequestError"))?;
let rsult: serde_json::Value =
serde_json::from_str(&access_token_data).map_err(|e| Error::throw("", Some(e)))?;
let id_token = if let Some(id_token) = rsult.get("id_token") {
let b = id_token.as_str().unwrap().to_bytes();
String::from_utf8(b.to_vec()).unwrap()
} else {
"".to_string()
};
let token_info_data = http_client
.do_get(&format!("{}?id_token={}", *GOOGLE_TOKEN_INFO_URL, id_token))
.await
.map_err(|_e| Error::invalid_request("InvalidRequestError"))?;
match serde_json::from_str::<GoogleJWT>(&token_info_data) {
Ok(jwt) => {
let user_name = if let Some(email) = &jwt.email {
email.to_owned()
} else {
return request.text(200, "");
};
let real_name = if let (Some(name1), Some(name2)) = (&jwt.family_name, &jwt.given_name)
{
Some(format!("{}{}", name1, name2))
} else if let Some(name1) = &jwt.family_name {
Some(name1.to_string())
} else {
jwt.given_name.as_ref().map(|name2| name2.to_string())
};
let request_with = request
.headers()
.get("X-Requested-With")
.map(|val| val.to_str().unwrap_or_default())
.unwrap_or("");
let user_id =
save_user_by_google(user_name, jwt.email, real_name, context.mysql()).await?;
session
.renew()
.insert_user_id(UserId(user_id))
.map_err(|e| Error::UnexpectedError(e.into()))?;
if request_with == "fetch" {
return request.json(200, R::ok("/"));
}
Ok(utils::see_other("/"))
}
Err(e) => request.text(200, &e.to_string()),
}
}
pub async fn gt(_request: HttpRequest) -> impl Responder {
#[allow(non_snake_case)]
#[rustfmt::skip]
let scopes = GITHUB_OAUTH_SCOPES.iter().map(|f|urlencoding::encode(f).to_string()).collect::<Vec<String>>().join("+"); let cb_url = urlencoding::encode(&GITHUB_CALLBACK_URL);
let random_bytes: Vec<u8> = (0..16).map(|_| rng().random::<u8>()).collect();
let state_code = GeneralPurpose::new(&alphabet::URL_SAFE, NO_PAD).encode(random_bytes);
log::info!("state_code={}", state_code);
#[allow(non_snake_case)]
let GITHUB_OAUTH_CONSENT_SCREEN_URL = format!(
"{}?response_type={}&client_id={}&state={}&redirect_uri={}&scope={}",
*GITHUB_OAUTH_URL, "code", *GITHUB_CLIENT_ID, state_code, cb_url, scopes
);
utils::send_redirect(&GITHUB_OAUTH_CONSENT_SCREEN_URL)
}
#[rustfmt::skip]
pub async fn gt_cb(
query: web::Query<HashMap<String, String>>,
context: web::Data<AppContext>,
session: TypedSession,
request: HttpRequest,
) -> impl Responder {
let code = query.get("code").ok_or_else(|| {
Error::invalid_request("Missing query string parameter: prefix")
})?;
let _state = query.get("state").ok_or_else(|| {
Error::invalid_request("Missing query string parameter: prefix")
})?;
let scopes = GITHUB_OAUTH_SCOPES
.iter()
.map(|f| urlencoding::encode(f).to_string())
.collect::<Vec<String>>()
.join(" ");
let params: Vec<(&str, &str)> = vec![
("scope", &scopes),
("redirect_uri", GITHUB_CALLBACK_URL.as_str()),
("grant_type", "authorization_code"),
("code", code),
];
let b64_credential = {
let urlencoded_id: String = form_urlencoded::byte_serialize(GITHUB_CLIENT_ID.as_bytes()).collect();
let urlencoded_secret: String = form_urlencoded::byte_serialize(GITHUB_CLIENT_SECRET.as_bytes()).collect();
GeneralPurpose::new(&alphabet::URL_SAFE, NO_PAD) .encode(format!("{}:{}", &urlencoded_id, urlencoded_secret))
};
let http_client = reqwest::Client::builder()
.use_rustls_tls()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(60))
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap()
.post(GITHUB_ACCESS_TOKEN_URL.to_string())
.header(
http::header::ACCEPT,
http::HeaderValue::from_static("application/json"),
)
.header(
http::header::CONTENT_TYPE,
http::HeaderValue::from_static("application/x-www-form-urlencoded"),
)
.header(
http::header::AUTHORIZATION,
http::HeaderValue::from_str(&format!("Basic {}", &b64_credential)).unwrap(),
)
.body(
form_urlencoded::Serializer::new(String::new())
.extend_pairs(params)
.finish()
.into_bytes(),
).send()
.await;
let token_res = match http_client{
Ok(res) => match res.error_for_status() {
Ok(o) => {
log::debug!(
"HttpRequest-Success: method=POST, url={}, StatusCode={}",
*GITHUB_ACCESS_TOKEN_URL,
o.status()
);
o.text().await.unwrap()
}
Err(e) => {
log::error!(
"HttpRequest-Failed: method=POST, url={}, error={:?}",
*GITHUB_ACCESS_TOKEN_URL,
e
);
"".to_string()
}
},
Err(e) => {
log::error!("HttpRequest-Error: method=POST, url={}, error={:?}", *GITHUB_ACCESS_TOKEN_URL, e);
"".to_string()
}
};
let token = serde_json::from_str::<GitHubToken>(&token_res).unwrap().access_token.unwrap();
log::info!("token_res={}", token);
let http_client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(60))
.build()
.unwrap()
.get(GITHUB_TOKEN_INFO_URL.to_string())
.header(
http::header::ACCEPT,
http::HeaderValue::from_static("application/vnd.github+json"),
)
.header(
http::header::AUTHORIZATION,
http::HeaderValue::from_str(&format!("Bearer {}", &token)).unwrap(),
)
.header(
"X-GitHub-Api-Version",
http::HeaderValue::from_str("2022-11-28").unwrap(),
)
.header(
"User-Agent",
http::HeaderValue::from_str("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36").unwrap(),
)
.send()
.await;
let user_info = match http_client{
Ok(res) => match res.error_for_status() {
Ok(o) => {
log::debug!(
"HttpRequest-Success: method=GET, url={}, StatusCode={}",
*GITHUB_TOKEN_INFO_URL,
o.status()
);
o.text().await.unwrap()
}
Err(e) => {
log::error!(
"HttpRequest-Failed: method=GET, url={}, error={}",
*GITHUB_TOKEN_INFO_URL,
e
);
"".to_string()
}
},
Err(e) => {
log::error!("HttpRequest-Error: method=GET, url={}, error={}", *GITHUB_TOKEN_INFO_URL, e);
"".to_string()
}
};
match serde_json::from_str::<GitHubUserInfo>(&user_info) {
Ok(userinfo) => {
let user_name = if let Some(name) = &userinfo.name {
name.to_owned()
} else {
return request.text(200, "");
};
let request_with = request
.headers()
.get("X-Requested-With")
.map(|val| val.to_str().unwrap_or_default())
.unwrap_or("");
let user_id =
save_user_by_github(user_name, userinfo.email, context.mysql()).await?;
session.renew().insert_user_id(UserId(user_id.to_owned()))
.map_err(|e| Error::UnexpectedError(e.into()))?;
if request_with == "fetch" {
return request.json(200, R::ok("/"));
}
Ok(utils::see_other("/"))
}
Err(e) => request.text(200, &e.to_string()),
}
}