zero4rs 2.0.0

zero4rs is a powerful, pragmatic, and extremely fast web framework for Rust
Documentation
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::services::user_service::save_user;
use crate::utils;

#[rustfmt::skip]
lazy_static! {
    // google
    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";

    // github
    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";
}

// ###################################################
// ### https://console.cloud.google.com/ (创建应用)
// ### https://console.developers.google.com/apis (创建应用的 Oauth Credentials)
// ###################################################
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()),
    }
}

// ###################################################
// ### https://github.com/settings/apps (创建应用)
// ###################################################
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("+"); // public_repo+user%3Aemail
    let cb_url = urlencoding::encode(&GITHUB_CALLBACK_URL); // redirect_uri=

    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")
    })?;

    // TODO!!! verify state_code === state_code as we are sended
    // log::info!("state_code={}", state);

    let scopes = GITHUB_OAUTH_SCOPES
        .iter()
        .map(|f| urlencoding::encode(f).to_string())
        .collect::<Vec<String>>()
        .join(" "); // public_repo+user%3Aemail

    let params: Vec<(&str, &str)> = vec![
        ("scope", &scopes),
        ("redirect_uri", GITHUB_CALLBACK_URL.as_str()),
        ("grant_type", "authorization_code"),
        ("code", code),
        // ("code_verifier", pkce_verifier.secret())
    ];

    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);
    // https://docs.github.com/en/rest/users?apiVersion=2022-11-28#get-a-single-user
    // curl -L \
    // -H "Accept: application/vnd.github+json" \
    // -H "Authorization: Bearer <YOUR-TOKEN>" \
    // -H "X-GitHub-Api-Version: 2022-11-28" \
    // https://api.github.com/user

    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()
            .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()),
    }
}