Add log in and log out support

O01eg
Oct 14, 2022, 8:46 AM
FUCFD4UVRUXDHG24MYEHF2E5E23BIFVKTRGLNBPBVKFLONVSMHTAC

Dependencies

  • [2] 6CFNBL5L Add headers for better security
  • [3] KDYHFAE3 Add and remove cookies
  • [4] AAUEQMCP Add common header block
  • [5] HZDCKIXQ Use constants for templates
  • [6] LWCZDLBI Add buttons to accept and remove cookies
  • [7] WVHXYKCV Add postgresql pools
  • [8] 2MPJPGRY Populate cookies jar
  • [9] HDHALX3U Add Cookies Policy page
  • [10] EVP2FSBH Split index page
  • [11] QEK76JYT Process and log template render error
  • [*] 65A3LIWU Use handlebars to render index
  • [*] TRBYOQBI Check CSRF and user existence
  • [*] 3HT5CE6S Manage TTL duration in config
  • [*] 4MZ4VIR7 Initial commit
  • [*] WLWTNO4Y Create form to request game password change link
  • [*] DGGFYSEG Use non-escaped template for Atom XML
  • [*] S6MX4MFO Add handlers for accepting and removing cookies

Change contents

  • replacement in src/templates/sub/header.html at line 5
    [4.147][4.147:162]()
    Log In Log Out
    [4.147]
    [4.162]
    {{#if common_auth_info.user}}
    Hello, {{ common_auth_info.user }}
    <form id="logout" action="logout.do" method="post"><input name="submit" type="submit" value="Log Out"></form>
    {{else}}
    <a href="login.html">Log In</a>
    {{/if}}
  • file addition: login.html (----------)
    [13.12]
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <link rel="alternate" type="application/rss+xml" href="static/rss.xml" title="Multiplayer FreeOrion server news" />
    <link rel="stylesheet" type="text/css" href="static/style.css" />
    <title>Welcome to public multiplayer FreeOrion server!</title>
    </head>
    <body>
    {{> header}}
    <div class="content">
    <h1>Log In</h1>
    <form id="login" action="login.do" method="post">
    <fieldset>
    <legend>Enter contact data to request password change:</legend>
    <div>
    <label for="login">Username:</label>
    <input name="login" id="login" type="text" placeholder="Username" required autofocus />
    </div>
    <div>
    <label for="password">Password:</label>
    <input name="password" id="password" type="password" placeholder="Password" required autofocus />
    </div>
    <input name="csrf" type="hidden" value="{{ csrf }}">
    <input name="submit" type="submit" value="Log In">
    </fieldset>
    </form>
    </div>
    {{> footer}}
    </body>
    </html>
  • replacement in src/pages/mod.rs at line 1
    [5.50][2.208:244]()
    use actix_web::HttpResponseBuilder;
    [5.50]
    [5.383]
    use actix_web::{cookie, HttpRequest, HttpResponseBuilder};
  • edit in src/pages/mod.rs at line 10
    [5.66]
    [5.1060]
    pub mod log_in;
    pub mod log_out;
  • edit in src/pages/mod.rs at line 25
    [5.744]
    [5.744]
    pub const LOG_IN: &str = "log-in";
  • edit in src/pages/mod.rs at line 36
    [5.1367]
    [5.414]
    #[derive(serde_derive::Serialize)]
    pub struct CommonAuthInfo {
    pub user: Option<String>,
    }
  • edit in src/pages/mod.rs at line 46
    [14.2179]
    [15.140]
    pub cache_login: Mutex<TtlCache<Uuid, ()>>,
  • edit in src/pages/mod.rs at line 70
    [2.834]
    [2.834]
    }
    pub fn request_to_jar(request: HttpRequest) -> cookie::CookieJar {
    let mut jar = cookie::CookieJar::new();
    match request.cookies() {
    Ok(cookies) => {
    for cookie in &*cookies {
    jar.add_original(cookie.clone());
    }
    }
    Err(e) => {
    log::error!("Cann't get cookies from request: {}", e);
    }
    }
    jar
  • file addition: log_out.rs (----------)
    [5.17]
    use actix_web::http::header;
    use actix_web::{cookie, web, HttpResponse};
    use crate::WebData;
    pub async fn log_out(data: web::Data<WebData<'_>>) -> HttpResponse {
    let mut response = HttpResponse::Found()
    .append_header((header::LOCATION, "index.html"))
    .finish();
    let mut builder = cookie::Cookie::build("auth", "").path("/");
    if data.set_cookie_domain {
    builder = builder.domain(data.base_domain.to_string());
    }
    if let Err(e) = response.add_removal_cookie(&builder.finish()) {
    log::error!("Cann't set cookie {}", e);
    }
    response
    }
  • file addition: log_in.rs (----------)
    [5.17]
    use actix_web::http::header;
    use actix_web::{cookie, web, HttpRequest, HttpResponse};
    use uuid::Uuid;
    use crate::pages::templates::LOG_IN;
    use crate::pages::{insert_security_headers, request_to_jar};
    use crate::{DataBaseRo, WebData};
    #[derive(serde_derive::Serialize)]
    struct PageData {
    csrf: Uuid,
    }
    #[derive(serde_derive::Deserialize)]
    pub struct FormData {
    login: String,
    password: String,
    csrf: Uuid,
    }
    pub async fn get_log_in(data: web::Data<WebData<'_>>) -> HttpResponse {
    let csrf = Uuid::new_v4();
    {
    let mut cache = data.cache_login.lock().await;
    cache.insert(
    csrf,
    (),
    std::time::Duration::from_secs(data.cache_duration_sec),
    );
    }
    let body = match data.handlebars.render(LOG_IN, &PageData { csrf }) {
    Ok(b) => b,
    Err(e) => {
    log::error!("Render login error: {}", e);
    return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
    }
    };
    insert_security_headers(HttpResponse::Ok()).body(body)
    }
    pub async fn post_log_in(
    request: HttpRequest,
    form: web::Form<FormData>,
    data: web::Data<WebData<'_>>,
    data_ro: web::Data<DataBaseRo>,
    ) -> HttpResponse {
    let cached_data = {
    let mut cache = data.cache_login.lock().await;
    cache.remove(&form.csrf)
    };
    if cached_data.is_none() {
    log::warn!("Unknown data for CSRF: {}", form.csrf);
    return HttpResponse::BadRequest().body("Incorrect");
    }
    let dbclient = match data_ro.0.get().await {
    Ok(c) => c,
    Err(e) => {
    log::error!("Pool RO error {}", e);
    return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
    }
    };
    let stmt = match dbclient.prepare("select web_password = crypt($1, web_password) from auth.users where player_name = $2 limit 1;").await {
    Ok(stmt) => stmt,
    Err(e) => {
    log::error!("Pool RO statement error {}", e);
    return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
    }
    };
    let rows = match dbclient
    .query_opt(&stmt, &[&form.password, &form.login])
    .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(row) => row,
    None => {
    return HttpResponse::NotFound().body("Not found");
    }
    };
    if !row.get::<_, bool>(0) {
    return HttpResponse::NotFound().body("Not found");
    }
    let mut jar = request_to_jar(request);
    let mut builder = cookie::Cookie::build("auth", form.login.clone())
    .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());
    let mut response = HttpResponse::Found()
    .append_header((header::LOCATION, "index.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
    }
  • replacement in src/pages/index.rs at line 1
    [5.100][5.101:137]()
    use actix_web::{web, HttpResponse};
    [5.100]
    [5.137]
    use actix_web::{web, HttpRequest, HttpResponse};
  • edit in src/pages/index.rs at line 3
    [5.138][2.837:880]()
    use crate::pages::insert_security_headers;
  • edit in src/pages/index.rs at line 4
    [5.1404]
    [5.562]
    use crate::pages::CommonAuthInfo;
    use crate::pages::{insert_security_headers, request_to_jar};
  • replacement in src/pages/index.rs at line 8
    [5.167][5.583:650](),[5.650][5.1405:1463]()
    pub async fn index(data: web::Data<WebData<'_>>) -> HttpResponse {
    let body = match data.handlebars.render(INDEX, &()) {
    [5.167]
    [5.314]
    #[derive(serde_derive::Serialize)]
    struct PageData {
    pub common_auth_info: CommonAuthInfo,
    }
    pub async fn index(request: HttpRequest, data: web::Data<WebData<'_>>) -> HttpResponse {
    let jar = request_to_jar(request);
    let user = jar
    .private(&data.cookies_key)
    .get("auth")
    .map(|x| x.value().to_string());
    let body = match data.handlebars.render(
    INDEX,
    &PageData {
    common_auth_info: CommonAuthInfo { user },
    },
    ) {
  • edit in src/pages/cookies_policy.rs at line 4
    [5.826][5.826:869]()
    use crate::pages::insert_security_headers;
  • edit in src/pages/cookies_policy.rs at line 5
    [5.914]
    [5.914]
    use crate::pages::{insert_security_headers, request_to_jar};
  • edit in src/pages/cookies_policy.rs at line 13
    [5.935][3.34:97](),[3.97][5.156:470](),[5.156][5.156:470](),[5.470][3.98:108](),[3.108][5.470:471](),[5.470][5.470:471]()
    fn request_to_jar(request: HttpRequest) -> cookie::CookieJar {
    let mut jar = cookie::CookieJar::new();
    match request.cookies() {
    Ok(cookies) => {
    for cookie in &*cookies {
    jar.add_original(cookie.clone());
    }
    }
    Err(e) => {
    log::error!("Cann't get cookies from request: {}", e);
    }
    }
    jar
    }
  • edit in src/main.rs at line 16
    [5.423]
    [17.1800]
    use pages::log_in::{get_log_in, post_log_in};
    use pages::log_out::log_out;
  • edit in src/main.rs at line 174
    [5.1587]
    [18.245]
    handlebars
    .register_template_string(templates::LOG_IN, include_str!("templates/login.html"))
    .expect("cookies policy template");
  • edit in src/main.rs at line 217
    [14.2306]
    [15.203]
    cache_login: tokio::sync::Mutex::new(ttl_cache::TtlCache::new(cache_capacity)),
  • edit in src/main.rs at line 261
    [5.1858]
    [19.451]
    .route("/login.html", web::get().to(get_log_in))
  • edit in src/main.rs at line 264
    [19.607]
    [5.2328]
    .route("/logout.do", web::post().to(log_out))
    .route("/login.do", web::post().to(post_log_in))