Log in with Mastodon

O01eg
Jul 31, 2024, 6:39 PM
W2AVMCLON3ANLDK6LSDEYLOLMC3VYX267ESPG4SYWJUQ2QLNWDEQC

Dependencies

  • [2] 5XW3RJKJ Add nodeinfo to well known
  • [3] HBDTKI2B Add auth info to password reset
  • [4] EKDGFVDQ Update game password from personal page
  • [5] IEFJTEII Update handlebars dependency
  • [6] V47NQLKF Use individual redirect url for each Mastodon domain
  • [7] EVP2FSBH Split index page
  • [8] YQFDKZIU Put FreeOrion version to config for simple update
  • [9] MFJBQU5F Fix check if web password wasn't set
  • [10] LWCZDLBI Add buttons to accept and remove cookies
  • [11] WW3KRXX6 Add page for reset game password
  • [12] HDHALX3U Add Cookies Policy page
  • [13] KDYHFAE3 Add and remove cookies
  • [14] CMA5SKJ3 Copy turns Atom generator
  • [15] S6MX4MFO Add handlers for accepting and removing cookies
  • [16] BVCWJKEX Get auth info for slow game page
  • [17] D3RL62X5 Implement revoking delegation
  • [18] H6GGDVHW Show auth info in reset password page
  • [19] MCF5COUL Add personal page
  • [20] 6NYILMKI Add page for slow game
  • [21] TRBYOQBI Check CSRF and user existence
  • [22] LTQCLSBU Split database usage in pages
  • [23] ZQIIC7C3 Add field to store timestamp of joining game
  • [24] YDWTHWAI Show form to revoke delegation
  • [25] WLWTNO4Y Create form to request game password change link
  • [26] RPAQDOZ4 Move atom pages to separate module
  • [27] FUCFD4UV Add log in and log out support
  • [28] V5ULHL43 Implement delegation support
  • [29] 6CFNBL5L Add headers for better security
  • [30] JG2BQCRD Implement query for delegation
  • [31] BCMU6UYK Start login mastodon form
  • [32] MUTHALNP Detect user and domain in Mastodon fediverse
  • [33] G4JCZ5F7 Store try to register on Mastodon domain
  • [34] RSIBXP3S Fix redirect URL
  • [35] WEVOADLS Require to accept cookies policy for log in
  • [36] 7QCJHYB6 Show contacts in personal page
  • [37] B5D2IKSB Use Cow for user name
  • [38] 5RQCVFRH Start leave game form
  • [39] OJO4B4QO Add login form and empty handler
  • [40] HTYEGVBU Add data to reset password page
  • [41] XTHO73VK Implement leave game
  • [42] MGRTVGLJ Redirect to Mastodon domain
  • [43] 5UYVIBUM Update game password from page
  • [44] HZDCKIXQ Use constants for templates
  • [45] 2MPJPGRY Populate cookies jar
  • [46] H7NQUYI6 Record join to the game
  • [47] YUFPADNO Add team info to slow game page data
  • [48] KDKRTAYJ Register application on Mastodon domain
  • [49] WVHXYKCV Add postgresql pools
  • [50] NY766BOQ Accept form to add player
  • [*] 4MZ4VIR7 Initial commit
  • [*] 3AKTNR3A Update deadpool-postgres dependency
  • [*] BCXEUKX6 Add config, static files and web server

Change contents

  • replacement in src/pages/slow_game.rs at line 136
    [7.195][7.195:234]()
    let jar = request_to_jar(request);
    [7.195]
    [7.2400]
    let jar = request_to_jar(&request);
  • replacement in src/pages/slow_game.rs at line 423
    [7.645][7.645:684]()
    let jar = request_to_jar(request);
    [7.645]
    [7.684]
    let jar = request_to_jar(&request);
  • replacement in src/pages/slow_game.rs at line 491
    [7.1293][7.1293:1332]()
    let jar = request_to_jar(request);
    [7.1293]
    [7.1332]
    let jar = request_to_jar(&request);
  • replacement in src/pages/slow_game.rs at line 559
    [7.657][7.657:696]()
    let jar = request_to_jar(request);
    [7.657]
    [7.696]
    let jar = request_to_jar(&request);
  • replacement in src/pages/slow_game.rs at line 628
    [7.2197][7.2197:2236]()
    let jar = request_to_jar(request);
    [7.2197]
    [7.2236]
    let jar = request_to_jar(&request);
  • replacement in src/pages/slow_game.rs at line 695
    [7.2809][7.2809:2848]()
    let jar = request_to_jar(request);
    [7.2809]
    [7.2848]
    let jar = request_to_jar(&request);
  • replacement in src/pages/reset_game_pwd.rs at line 35
    [7.189][7.208:247]()
    let jar = request_to_jar(request);
    [7.189]
    [7.247]
    let jar = request_to_jar(&request);
  • replacement in src/pages/reset_game_pwd.rs at line 140
    [4.932][4.932:971]()
    let jar = request_to_jar(request);
    [4.932]
    [4.971]
    let jar = request_to_jar(&request);
  • replacement in src/pages/query_reset_game_pwd.rs at line 29
    [3.710][3.710:749]()
    let jar = request_to_jar(request);
    [3.710]
    [3.749]
    let jar = request_to_jar(&request);
  • replacement in src/pages/my.rs at line 39
    [7.1586][7.1586:1625]()
    let jar = request_to_jar(request);
    [7.1586]
    [7.1625]
    let jar = request_to_jar(&request);
  • replacement in src/pages/mod.rs at line 82
    [7.1552][7.1552:1619]()
    pub fn request_to_jar(request: HttpRequest) -> cookie::CookieJar {
    [7.1552]
    [7.1619]
    pub fn request_to_jar(request: &HttpRequest) -> cookie::CookieJar {
  • replacement in src/pages/log_in.rs at line 40
    [7.3342][7.95:134]()
    let jar = request_to_jar(request);
    [7.3342]
    [7.134]
    let jar = request_to_jar(&request);
  • edit in src/pages/log_in.rs at line 61
    [7.3679]
    [7.3679]
    fn log_in_response(
    player_name: &str,
    request: &HttpRequest,
    cookies_key: &actix_web::cookie::Key,
    set_cookie_domain: bool,
    base_domain: &str,
    ) -> HttpResponse {
    let mut jar = request_to_jar(request);
    if !jar
    .private(cookies_key)
    .get("i_accept_cookie")
    .map_or(false, |x| x.value() == "yes")
    {
    return HttpResponse::Found()
    .append_header((header::LOCATION, "cookies-policy.html"))
    .finish();
    }
    let mut builder = cookie::Cookie::build("auth", player_name.to_lowercase())
    .path("/")
    .secure(true)
    .http_only(true)
    .same_site(cookie::SameSite::Strict)
    .max_age(cookie::time::Duration::weeks(4));
    if set_cookie_domain {
    builder = builder.domain(base_domain.to_string());
    }
    jar.private_mut(cookies_key).add(builder.finish());
    let mut response = HttpResponse::Found()
    .append_header((header::LOCATION, "my.html"))
    .finish();
    if let Some(c) = jar.get("auth") {
    if let Err(e) = response.add_cookie(c) {
    log::error!("Cann't set cookie {}", e);
    }
    }
    response
    }
  • edit in src/pages/log_in.rs at line 149
    [7.5287][7.5287:5337](),[7.5337][7.405:668]()
    }
    let mut jar = request_to_jar(request);
    if !jar
    .private(&data.cookies_key)
    .get("i_accept_cookie")
    .map_or(false, |x| x.value() == "yes")
    {
    return HttpResponse::Found()
    .append_header((header::LOCATION, "cookies-policy.html"))
    .finish();
  • replacement in src/pages/log_in.rs at line 151
    [7.675][7.1975:2054](),[7.2054][7.5409:5736](),[7.5409][7.5409:5736]()
    let mut builder = cookie::Cookie::build("auth", form.login.to_lowercase())
    .path("/")
    .secure(true)
    .http_only(true)
    .same_site(cookie::SameSite::Strict)
    .max_age(cookie::time::Duration::weeks(4));
    if data.set_cookie_domain {
    builder = builder.domain(data.base_domain.to_string());
    }
    jar.private_mut(&data.cookies_key).add(builder.finish());
    [7.675]
    [7.5736]
    log_in_response(
    &form.login,
    &request,
    &data.cookies_key,
    data.set_cookie_domain,
    &data.base_domain,
    )
    }
  • replacement in src/pages/log_in.rs at line 160
    [7.5737][7.5737:5782](),[7.5782][7.3922:3976](),[7.3976][7.5839:6027](),[7.5839][7.5839:6027]()
    let mut response = HttpResponse::Found()
    .append_header((header::LOCATION, "my.html"))
    .finish();
    if let Some(c) = jar.get("auth") {
    if let Err(e) = response.add_cookie(c) {
    log::error!("Cann't set cookie {}", e);
    }
    }
    response
    [7.5737]
    [7.106]
    fn get_mastodon_redirect_url(base_proto: &str, base_domain: &str, domain: &str) -> String {
    format!(
    "{}://{}/mastodon-redirect-{}.html",
    base_proto, base_domain, domain
    )
  • replacement in src/pages/log_in.rs at line 180
    [7.382][6.0:84]()
    let website_redirect = format!("{}mastodon-redirect-{}.html", website, domain);
    [7.382]
    [7.382]
    let website_redirect = get_mastodon_redirect_url(&data.base_proto, &data.base_domain, domain);
  • edit in src/pages/log_in.rs at line 237
    [7.2575]
    [7.2575]
    Err(())
    }
    }
    }
    async fn obtain_token_mastodon(
    base_proto: &str,
    base_domain: &str,
    domain: &str,
    client_id: &str,
    client_secret: &str,
    code: &str,
    ) -> Result<String, ()> {
    let client = awc::Client::default();
    let website_redirect = get_mastodon_redirect_url(base_proto, base_domain, domain);
    match client
    .post(format!("https://{}/oauth/token", domain))
    .content_type("application/x-www-form-urlencoded")
    .send_body(format!(
    "client_id={}&client_secret={}&redirect_uri={}&grant_type=authorization_code&code={}&scopes=read:accounts",
    pct_str::PctString::encode(client_id.chars(), pct_str::URIReserved).as_str(),
    pct_str::PctString::encode(client_secret.chars(), pct_str::URIReserved).as_str(),
    pct_str::PctString::encode(website_redirect.chars(), pct_str::URIReserved).as_str(),
    pct_str::PctString::encode(code.chars(), pct_str::URIReserved).as_str()
    ))
    .await {
    Ok(mut resp) => match resp.json::<serde_json::Value>().await {
    Ok(serde_json::Value::Object(resp_value_obj)) => {
    let access_token_value = resp_value_obj.get("access_token");
    if let Some(serde_json::Value::String(access_token)) = access_token_value {
    Ok(access_token.clone())
    } else {
    log::error!(
    "Mastodon domain {} login failed missing fields JSON {:?}",
    domain,
    resp_value_obj
    );
    Err(())
    }
    }
    Ok(resp_value) => {
    log::error!(
    "Mastodon domain {} login failed non-object JSON {:?}",
    domain,
    resp_value
    );
    Err(())
    }
    Err(err) => {
    log::error!(
    "Mastodon domain {} login failed JSON {:?}",
    domain,
    err
    );
    Err(())
    }
    }
    Err(err) => {
    log::error!(
    "Mastodon domain {} login failed {:?}",
    domain,
    err
    );
    Err(())
    }
    }
    }
    async fn check_token_mastodon(domain: &str, token: &str) -> Result<String, ()> {
    let client = awc::Client::default();
    match client
    .get(format!(
    "https://{}/api/v1/accounts/verify_credentials",
    domain
    ))
    .insert_header(("Authorization", format!("Bearer {}", token)))
    .send()
    .await
    {
    Ok(mut resp) => match resp.json::<serde_json::Value>().await {
    Ok(serde_json::Value::Object(resp_value_obj)) => {
    let username_value = resp_value_obj.get("username");
    if let Some(serde_json::Value::String(username)) = username_value {
    Ok(username.clone())
    } else {
    log::error!(
    "Mastodon domain {} login failed missing fields JSON {:?}",
    domain,
    resp_value_obj
    );
    Err(())
    }
    }
    Ok(resp_value) => {
    log::error!(
    "Mastodon domain {} login failed non-object JSON {:?}",
    domain,
    resp_value
    );
    Err(())
    }
    Err(err) => {
    log::error!("Mastodon domain {} login failed JSON {:?}", domain, err);
    Err(())
    }
    },
    Err(err) => {
    log::error!("Mastodon domain {} login failed {:?}", domain, err);
  • edit in src/pages/log_in.rs at line 348
    [7.2611]
    [7.2611]
    }
    async fn player_name_by_mastodon(
    dbclient: &deadpool::managed::Object<deadpool_postgres::Manager>,
    user: &str,
    domain: &str,
    ) -> Result<Option<String>, ()> {
    let stmt = match dbclient.prepare("select player_name from auth.contacts where protocol = 'mastodon' and is_active = true and delete_ts is null and address = $1 limit 1;").await {
    Ok(stmt) => stmt,
    Err(e) => {
    log::error!("Pool RO statement error {}", e);
    return Err(())
    }
    };
    let address = format!("{}@{}", user, domain);
    let rows = match dbclient.query_opt(&stmt, &[&address]).await {
    Ok(rows) => rows,
    Err(e) => {
    log::error!("Pool RO query error {}", e);
    return Err(());
    }
    };
    let row = match rows {
    Some(row) => row,
    None => {
    return Ok(None);
    }
    };
    Ok(Some(row.get::<_, String>(0)))
  • replacement in src/pages/log_in.rs at line 413
    [7.736][7.736:1114]()
    let stmt = match dbclient.prepare("select player_name from auth.contacts where protocol = 'mastodon' and is_active = true and delete_ts is null and address = $1 limit 1;").await {
    Ok(stmt) => stmt,
    Err(e) => {
    log::error!("Pool RO statement error {}", e);
    return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
    [7.736]
    [7.1114]
    let player_name = match player_name_by_mastodon(&dbclient, &user, &domain).await {
    Ok(Some(player_name)) => player_name,
    Ok(None) => {
    return HttpResponse::NotFound().body("Not found");
  • replacement in src/pages/log_in.rs at line 419
    [7.1124][7.1124:1349]()
    };
    let address = format!("{}@{}", user, domain);
    let rows = match dbclient.query_opt(&stmt, &[&address]).await {
    Ok(rows) => rows,
    Err(e) => {
    log::error!("Pool RO query error {}", e);
    [7.1124]
    [7.1349]
    Err(()) => {
  • edit in src/pages/log_in.rs at line 423
    [7.1456][7.1456:1607]()
    let row = match rows {
    Some(row) => row,
    None => {
    return HttpResponse::NotFound().body("Not found");
    }
    };
  • edit in src/pages/log_in.rs at line 424
    [7.1608][7.1608:1654]()
    let player_name = row.get::<_, &str>(0);
  • replacement in src/pages/log_in.rs at line 547
    [7.1][7.1:75](),[7.75][6.2220:2601]()
    let website = format!("{}://{}/", data.base_proto, data.base_domain);
    let website_redirect = format!("{}mastodon-redirect-{}.html", website, domain);
    let location = format!("https://{}/oauth/authorize?client_id={}&scope=read:accounts&redirect_uri={}&response_type=code", domain, pct_str::PctString::encode(client_id.chars(), pct_str::URIReserved).as_str(), pct_str::PctString::encode(website_redirect.chars(), pct_str::URIReserved).as_str());
    [7.1]
    [7.1531]
    let website_redirect = get_mastodon_redirect_url(&data.base_proto, &data.base_domain, &domain);
    let location = format!("https://{}/oauth/authorize?client_id={}&scope=read:accounts&redirect_uri={}&response_type=code",
    domain,
    pct_str::PctString::encode(client_id.chars(), pct_str::URIReserved).as_str(),
    pct_str::PctString::encode(website_redirect.chars(), pct_str::URIReserved).as_str(),);
  • edit in src/pages/log_in.rs at line 556
    [7.616]
    [7.6027]
    }
    #[actix_web::get("/mastodon-redirect-{domain}.html")]
    pub async fn get_log_in_mastodon_redirect(
    req: HttpRequest,
    domain: web::Path<String>,
    data: web::Data<WebData<'_>>,
    data_ro: web::Data<DataBaseRo>,
    ) -> HttpResponse {
    let domain = domain.into_inner();
    let domain = match domain.char_indices().nth(128) {
    None => domain,
    Some((idx, _)) => domain[..idx].to_string(),
    };
    let mut code = None;
    for params in req.query_string().split('&') {
    if let Some((k, v)) = params.split_once('=') {
    if k == "code" {
    code = Some(v);
    }
    }
    }
    let code = match code {
    Some(r) => r,
    None => {
    log::warn!(
    "Mastodon not redirected with code in {}",
    req.query_string()
    );
    return HttpResponse::BadRequest().body("Incorrect query");
    }
    };
    let dbclient = match data_ro.0.get().await {
    Ok(client) => client,
    Err(e) => {
    log::error!("{}", e);
    return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
    }
    };
    let stmt = match dbclient
    .prepare("select client_id, client_secret from auth.mastodon_apps where domain = $1;")
    .await
    {
    Ok(stmt) => stmt,
    Err(e) => {
    log::error!("{}", e);
    return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
    }
    };
    let rows = match dbclient.query_opt(&stmt, &[&domain]).await {
    Ok(rows) => rows,
    Err(e) => {
    log::error!("Pool RO query error {}", e);
    return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
    }
    };
    let row = match rows {
    Some(r) => r,
    None => {
    return HttpResponse::NotFound().body("Not found");
    }
    };
    let opt_client_id = row.get::<_, Option<&str>>(0);
    let opt_client_secret = row.get::<_, Option<&str>>(1);
    if let (Some(client_id), Some(client_secret)) = (opt_client_id, opt_client_secret) {
    match obtain_token_mastodon(
    &data.base_proto,
    &data.base_domain,
    &domain,
    client_id,
    client_secret,
    code,
    )
    .await
    {
    Ok(token) => match check_token_mastodon(&domain, &token).await {
    Ok(user) => match player_name_by_mastodon(&dbclient, &user, &domain).await {
    Ok(Some(player_name)) => log_in_response(
    &player_name,
    &req,
    &data.cookies_key,
    data.set_cookie_domain,
    &data.base_domain,
    ),
    Ok(None) => HttpResponse::NotFound().body("Not found"),
    Err(()) => {
    HttpResponse::ServiceUnavailable().body(actix_web::body::None::new())
    }
    },
    Err(()) => HttpResponse::NotFound().body("Not found"),
    },
    Err(()) => HttpResponse::NotFound().body("Not found"),
    }
    } else {
    log::warn!("Missing mastodon client data for domain {}", domain);
    HttpResponse::NotFound().body("Not found")
    }
  • replacement in src/pages/index.rs at line 14
    [7.6363][7.6363:6402]()
    let jar = request_to_jar(request);
    [7.6363]
    [7.6402]
    let jar = request_to_jar(&request);
  • replacement in src/pages/cookies_policy.rs at line 14
    [7.207][7.207:246]()
    let jar = request_to_jar(request);
    [7.207]
    [7.395]
    let jar = request_to_jar(&request);
  • replacement in src/pages/cookies_policy.rs at line 37
    [7.361][7.361:404]()
    let mut jar = request_to_jar(request);
    [7.361]
    [7.404]
    let mut jar = request_to_jar(&request);
  • replacement in src/main.rs at line 16
    [7.423][7.617:685]()
    use pages::log_in::{get_log_in, post_log_in, post_log_in_mastodon};
    [7.423]
    [7.6784]
    use pages::log_in::{get_log_in, get_log_in_mastodon_redirect, post_log_in, post_log_in_mastodon};
  • edit in src/main.rs at line 302
    [2.787]
    [7.1998]
    .service(get_log_in_mastodon_redirect)
  • edit in Cargo.toml at line 21
    [5.19]
    [53.0]
    deadpool = "0.12"
  • edit in Cargo.lock at line 869
    [54.20503]
    [7.4003]
    "deadpool",