From 8b094063399ffc5a894a7e8c0110a2827cf2a705 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Tue, 24 Sep 2024 22:19:55 +0200 Subject: [PATCH 01/28] login --- Cargo.toml | 1 + .../20240921193253_create_auth_table.sql | 5 +- routes.json | 4 +- src/main.rs | 80 +++++++++++++++++-- static/js/index.js | 2 +- 5 files changed, 81 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e0d427f..71a6ed6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ chrono = { version = "0.4.38", features = ["alloc"]} futures-util = "0.3.30" h2 = "0.4.6" argon2 = "0.5.3" +rand = "0.8.1" [target.'cfg(unix)'.dependencies] daemonize = "0.5.0" \ No newline at end of file diff --git a/migrations/20240921193253_create_auth_table.sql b/migrations/20240921193253_create_auth_table.sql index 0939fac..6df0ec6 100644 --- a/migrations/20240921193253_create_auth_table.sql +++ b/migrations/20240921193253_create_auth_table.sql @@ -2,8 +2,7 @@ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY NOT NULL, username TEXT NOT NULL, -salt text NOT NULL, -hash TEXT NOT NULL, +saltyhash text NOT NULL, permissions INTEGER DEFAULT 0, -token TEXT +token TEXT NOT NULL ) \ No newline at end of file diff --git a/routes.json b/routes.json index 4d230a9..a7d69df 100644 --- a/routes.json +++ b/routes.json @@ -2,5 +2,7 @@ "/": "static/html/index.html", "/results": "static/html/results.html", "/archives": "static/html/archives.html", - "/favicon.ico": "static/img/favicon.ico" + "/favicon.ico": "static/img/favicon.ico", + "/login": "static/html/login.html", + "/register": "static/html/register.html" } diff --git a/src/main.rs b/src/main.rs index 3be1819..d149ee8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use bytes::Bytes; use chrono::{DateTime, Utc}; use futures; use http_body_util::{BodyExt, Full}; -use hyper::body::Incoming; +use hyper::body::{Body, Incoming}; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Error, Method, Request, Response, StatusCode}; @@ -20,7 +20,14 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::SystemTime; use tokio::net::TcpListener; - +use rand::distributions::Standard; +use argon2::{ + password_hash::{ + rand_core::OsRng, + PasswordHash, PasswordHasher, PasswordVerifier, SaltString + }, + Argon2 +}; #[cfg(target_os = "linux")] use daemonize::Daemonize; #[derive(Serialize, Deserialize)] @@ -29,7 +36,7 @@ struct Player { name: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize)] struct Vote { plus_player_id: i64, plus_nickname: String, @@ -39,6 +46,19 @@ struct Vote { minus_reason: String, } +#[derive(Serialize, Deserialize)] +struct User { + username: String, + saltyhash: String, + permissions: i64, + token: String +} + +#[derive(Serialize, Deserialize)] +struct Login { + username: String, + password: String +} #[derive(Serialize, Deserialize)] struct Settings { database_url: String, @@ -178,13 +198,27 @@ async fn get_votes(req: &Request, db: Arc>) -> Vec, db: Arc>) -> Result>, Error> { let path = req.uri().path(); - if path != "/post" { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request (Bad Route)"))).unwrap()); + match path { + "/vote" => { + post_vote(req, db).await + }, + "/login" => { + login(req, db).await + }, + "/register" => { + not_found().await + }, + _ => { + not_found().await + } } +} + +async fn post_vote(req: Request, db: Arc>) -> Result>, Error> { let body = req.into_body().collect().await?; let data: Result = from_reader(body.aggregate().reader()); if data.is_err() { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request (Bad Data)"))).unwrap()); + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); } let vote = data.unwrap(); let timestamp: DateTime = DateTime::from(SystemTime::now()); @@ -206,6 +240,40 @@ async fn post(req: Request, db: Arc>) -> Result, db: Arc>) -> Result>, Error> { + let body = req.into_body().collect().await; + let data: Result = from_reader(body?.aggregate().reader()); + if data.is_err() { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); + } + let data = data.unwrap(); + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let result = sqlx::query!(r#"SELECT * FROM users WHERE username=?1"#, data.username).fetch_optional(&mut *conn).await; + match result { + Ok(Some(user)) => { + let argon = Argon2::default(); + let hash = PasswordHash::new(&user.saltyhash).unwrap(); + match argon.verify_password(data.password.as_bytes(), &hash) { + Ok(()) => { + Ok(Response::builder().header("Set-Cookie", format!("token={}", user.token)).body(Full::new(Bytes::from("Ok"))).unwrap()) + }, + Err(_) => { + Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) + } + } + }, + Ok(None) => { + Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) + } + Err(e) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Bad Request"))).unwrap()) } + } +} + +async fn register(req: Request, db: Arc>) -> Result>, Error> { + todo!() +} + async fn not_found() -> Result>, Error> { let mut file_path = env::current_dir().expect("Could not get app directory."); file_path.push("static/html/404.html"); diff --git a/static/js/index.js b/static/js/index.js index 67ba756..062d147 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -59,7 +59,7 @@ async function main() { true, "warning"); return; } - if (await fetch("/post", { + if (await fetch("/vote", { method: "post", body: JSON.stringify(vote) }) .then(r => r.status) === 200) { -- 2.47.3 From bcf3000bcc0a1d458ac56941c0aa5f9d367d7073 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Wed, 25 Sep 2024 23:13:15 +0200 Subject: [PATCH 02/28] token cookie, register --- .../20240921193253_create_auth_table.sql | 3 +- src/main.rs | 55 ++++++++++++++++--- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/migrations/20240921193253_create_auth_table.sql b/migrations/20240921193253_create_auth_table.sql index 6df0ec6..d0d0d70 100644 --- a/migrations/20240921193253_create_auth_table.sql +++ b/migrations/20240921193253_create_auth_table.sql @@ -4,5 +4,6 @@ id INTEGER PRIMARY KEY NOT NULL, username TEXT NOT NULL, saltyhash text NOT NULL, permissions INTEGER DEFAULT 0, -token TEXT NOT NULL +token TEXT NOT NULL, +UNIQUE(id, username) ) \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d149ee8..ec46d27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use bytes::Buf; use bytes::Bytes; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Days, Utc}; use futures; use http_body_util::{BodyExt, Full}; use hyper::body::{Body, Incoming}; @@ -20,7 +20,7 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::SystemTime; use tokio::net::TcpListener; -use rand::distributions::Standard; +use rand::distributions::{DistString, Alphanumeric}; use argon2::{ password_hash::{ rand_core::OsRng, @@ -206,7 +206,7 @@ async fn post(req: Request, db: Arc>) -> Result { - not_found().await + register(req, db).await }, _ => { not_found().await @@ -243,10 +243,13 @@ async fn post_vote(req: Request, db: Arc>) -> Result async fn login(req: Request, db: Arc>) -> Result>, Error> { let body = req.into_body().collect().await; let data: Result = from_reader(body?.aggregate().reader()); - if data.is_err() { + if data.is_err(){ return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); } let data = data.unwrap(); + if !check_username(&data.username) { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); + } let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); let result = sqlx::query!(r#"SELECT * FROM users WHERE username=?1"#, data.username).fetch_optional(&mut *conn).await; @@ -256,7 +259,10 @@ async fn login(req: Request, db: Arc>) -> Result { - Ok(Response::builder().header("Set-Cookie", format!("token={}", user.token)).body(Full::new(Bytes::from("Ok"))).unwrap()) + let date: DateTime = DateTime::from(SystemTime::now()); + let date = date.checked_add_days(Days::new(7)).unwrap(); + println!("{}", date.to_rfc2822()); + Ok(Response::builder().header("Set-Cookie", format!("token={}; Expires={}; Secure; HttpOnly; SameSite=Strict", user.token, date.to_rfc2822())).body(Full::new(Bytes::from("Ok"))).unwrap()) }, Err(_) => { Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) @@ -266,12 +272,47 @@ async fn login(req: Request, db: Arc>) -> Result { Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) } - Err(e) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Bad Request"))).unwrap()) } + Err(_) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Server Error"))).unwrap()) } } } async fn register(req: Request, db: Arc>) -> Result>, Error> { - todo!() + let body = req.into_body().collect().await; + let data: Result = from_reader(body?.aggregate().reader()); + if data.is_err() { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); + } + let data = data.unwrap(); + if !check_username(&data.username) { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); + } + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let exists = sqlx::query!(r#"SELECT id FROM users WHERE username=?1"#, data.username).fetch_optional(&mut *conn).await; + if exists.unwrap().is_some() { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); + } + let argon2 = Argon2::default(); + let hash = argon2.hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng)).unwrap().to_string(); + let token = Alphanumeric.sample_string(&mut OsRng, 256); + println!("{}", token); + let result = sqlx::query!(r#"INSERT INTO users ( username, saltyhash, permissions, token) VALUES ( ?1, ?2, ?3, ?4 )"#, data.username, hash, 0, token).execute(&mut *conn).await; + match result { + Ok(_) => Ok(Response::builder().body(Full::new(Bytes::from(""))).unwrap()), + Err(_) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Server Error"))).unwrap()) } + } +} + +fn check_username(username: &String) -> bool { + if username.len() > 21 { + return false; + } + for x in username.chars() { + if !x.is_ascii_alphanumeric() { + return false; + } + } + true } async fn not_found() -> Result>, Error> { -- 2.47.3 From 0a87f67824f16e4592166c95949fe9bda32cbbce Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Fri, 27 Sep 2024 18:22:51 +0200 Subject: [PATCH 03/28] Check password --- src/main.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index ec46d27..bc0b46e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -261,7 +261,6 @@ async fn login(req: Request, db: Arc>) -> Result { let date: DateTime = DateTime::from(SystemTime::now()); let date = date.checked_add_days(Days::new(7)).unwrap(); - println!("{}", date.to_rfc2822()); Ok(Response::builder().header("Set-Cookie", format!("token={}; Expires={}; Secure; HttpOnly; SameSite=Strict", user.token, date.to_rfc2822())).body(Full::new(Bytes::from("Ok"))).unwrap()) }, Err(_) => { @@ -286,6 +285,9 @@ async fn register(req: Request, db: Arc>) -> Result< if !check_username(&data.username) { return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); } + if !check_password(&data.password) { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); + } let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); let exists = sqlx::query!(r#"SELECT id FROM users WHERE username=?1"#, data.username).fetch_optional(&mut *conn).await; @@ -315,6 +317,28 @@ fn check_username(username: &String) -> bool { true } +fn check_password(password: &String) -> bool { + // one symbol, 10 chars min, one capital letter, one number + if password.len() < 10 { + return false; + } + let mut up = false; + let mut num = false; + let mut sym = false; + for c in password.chars() { + if c.is_uppercase() { + up = true; + } + if c.is_numeric() { + num = true; + } + if !c.is_alphanumeric() { + sym = true; + } + } + up && num && sym +} + async fn not_found() -> Result>, Error> { let mut file_path = env::current_dir().expect("Could not get app directory."); file_path.push("static/html/404.html"); -- 2.47.3 From 19fe28be49b5785c928f900587d885d1145cb208 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 28 Sep 2024 00:06:32 +0200 Subject: [PATCH 04/28] login page --- static/css/login.css | 86 ++++++++++++++++++++++++++++++++++++++++++ static/html/login.html | 37 ++++++++++++++++++ static/js/login.js | 18 +++++++++ 3 files changed, 141 insertions(+) create mode 100644 static/css/login.css create mode 100644 static/html/login.html create mode 100644 static/js/login.js diff --git a/static/css/login.css b/static/css/login.css new file mode 100644 index 0000000..8868c46 --- /dev/null +++ b/static/css/login.css @@ -0,0 +1,86 @@ +body { + background-color: #11111b; + padding:0; + margin:0; +} + +.footer { + margin: 0; + position: fixed; + bottom: 0; + width: 100%; + max-height: 50px; + display: flex; + background-color: #b4befe; + justify-content: center; +} + +.footer > a { + padding: 10px; + margin: 5px; + background-color: #94e2d5; + border-radius: 5px; + color: #1e1e2e; + font-family: "Segoe UI"; + font-size: 14px; +} + +.app { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, calc(-50% - 60px)); + background-color: #181926; + border-radius: 12px; + width: 100%; + max-width: 500px; + height: fit-content; + padding: 10px; +} + +.app > h1 { + margin-top: 5px; + color: yellow; +} + +.app > h3 { + color: red; +} + +.sub { + display: grid; + width: fit-content; + margin: auto; +} + +.sub > input { + margin-bottom: 5px; + width: 300px; + color: black; +} + +.sub > label { + color: white; +} + +.sub > button { + justify-self: right; + font-size: 14px; + margin: 4px 0 0 0; + padding: 5px; + background-color: cornflowerblue; + color: white; + border: none; + border-radius: 5px; +} + +.app > p { + text-align: center; + color: white; +} + +.app > a { + display: flow; + text-align: center; + color: white; +} \ No newline at end of file diff --git a/static/html/login.html b/static/html/login.html new file mode 100644 index 0000000..867ef5d --- /dev/null +++ b/static/html/login.html @@ -0,0 +1,37 @@ + + + + + + + + + + Login + + +
+

Connection

+

+
+ +
+ +
+ +
+ +
+ +
+

Tu n'as pas de compte?

+ Créer un compte. +
+ + + \ No newline at end of file diff --git a/static/js/login.js b/static/js/login.js new file mode 100644 index 0000000..b7b4e69 --- /dev/null +++ b/static/js/login.js @@ -0,0 +1,18 @@ +async function login() { + let username = document.getElementById("username").value; + let password = document.getElementById("password").value; + let code = await fetch("/login", + {method: "POST", body: JSON.stringify({"username": username, "password": password})}) + .then(r => { + if (r.status === 400) { + let message = document.getElementById("message"); + message.textContent = "Mauvais mot de passe/nom d'utilisateur!"; + } else { + window.location.href = "/"} + }); +} + +let button = document.getElementById("connect"); +button.addEventListener("click", () => { + let _ = login(); +}) \ No newline at end of file -- 2.47.3 From 10aa258576a7f5c24fcac243414bf5252e32ff35 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 28 Sep 2024 00:07:01 +0200 Subject: [PATCH 05/28] backend update (login modified and new logout) --- src/main.rs | 59 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/src/main.rs b/src/main.rs index bc0b46e..c6e9376 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,24 @@ +use argon2::{ + password_hash::{ + rand_core::OsRng, + PasswordHash, PasswordHasher, PasswordVerifier, SaltString, + }, + Argon2, +}; use bytes::Buf; use bytes::Bytes; use chrono::{DateTime, Days, Utc}; +#[cfg(target_os = "linux")] +use daemonize::Daemonize; use futures; use http_body_util::{BodyExt, Full}; use hyper::body::{Body, Incoming}; +use hyper::header::{LOCATION, SET_COOKIE}; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Error, Method, Request, Response, StatusCode}; use hyper_util::rt::{TokioIo, TokioTimer}; +use rand::distributions::{Alphanumeric, DistString}; use serde::{Deserialize, Serialize}; use serde_json::{from_reader, Value}; use sqlx::sqlite::SqlitePool; @@ -20,16 +31,6 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::SystemTime; use tokio::net::TcpListener; -use rand::distributions::{DistString, Alphanumeric}; -use argon2::{ - password_hash::{ - rand_core::OsRng, - PasswordHash, PasswordHasher, PasswordVerifier, SaltString - }, - Argon2 -}; -#[cfg(target_os = "linux")] -use daemonize::Daemonize; #[derive(Serialize, Deserialize)] struct Player { id: i64, @@ -51,13 +52,13 @@ struct User { username: String, saltyhash: String, permissions: i64, - token: String + token: String, } #[derive(Serialize, Deserialize)] struct Login { username: String, - password: String + password: String, } #[derive(Serialize, Deserialize)] struct Settings { @@ -74,10 +75,12 @@ async fn service(req: Request, db: Arc>) -> Result, db: Arc>) -> Result>, Error> { let path = req.uri().path(); - if path.starts_with("/static/") { + if path.starts_with("/static") { get_file(path).await - } else if path.starts_with("/data/") { + } else if path.starts_with("/data") { get_data(path, &req, db).await + } else if path.starts_with("/logout") { + logout().await } else { get_page(path).await } @@ -201,13 +204,13 @@ async fn post(req: Request, db: Arc>) -> Result { post_vote(req, db).await - }, + } "/login" => { login(req, db).await - }, + } "/register" => { register(req, db).await - }, + } _ => { not_found().await } @@ -243,7 +246,7 @@ async fn post_vote(req: Request, db: Arc>) -> Result async fn login(req: Request, db: Arc>) -> Result>, Error> { let body = req.into_body().collect().await; let data: Result = from_reader(body?.aggregate().reader()); - if data.is_err(){ + if data.is_err() { return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); } let data = data.unwrap(); @@ -261,13 +264,18 @@ async fn login(req: Request, db: Arc>) -> Result { let date: DateTime = DateTime::from(SystemTime::now()); let date = date.checked_add_days(Days::new(7)).unwrap(); - Ok(Response::builder().header("Set-Cookie", format!("token={}; Expires={}; Secure; HttpOnly; SameSite=Strict", user.token, date.to_rfc2822())).body(Full::new(Bytes::from("Ok"))).unwrap()) - }, + // With server side rendering, redirect here to "/" + Ok(Response::builder() + .header(SET_COOKIE, + format!("token={}; Expires={}; Secure; HttpOnly; SameSite=Strict", user.token, date.to_rfc2822())) + .body(Full::new(Bytes::from("Ok"))) + .unwrap()) + } Err(_) => { Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) } } - }, + } Ok(None) => { Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) } @@ -305,6 +313,15 @@ async fn register(req: Request, db: Arc>) -> Result< } } +async fn logout() -> Result>, Error> { + let date: DateTime = DateTime::from(SystemTime::now()); + Ok(Response::builder() + .status(StatusCode::SEE_OTHER) + .header(LOCATION, "/") + .header(SET_COOKIE, format!("token=''; Expires={}; Secure; HttpOnly; SameSite=Strict", date.to_rfc2822())) + .body(Full::new(Bytes::from(""))).unwrap()) +} + fn check_username(username: &String) -> bool { if username.len() > 21 { return false; -- 2.47.3 From 66486c3ceaef0f7c25996ce544e5760dfe9fc3af Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 28 Sep 2024 00:07:20 +0200 Subject: [PATCH 06/28] register progress --- static/html/register.html | 41 +++++++++++++++++++++++++++++++++++++++ static/js/register.js | 11 +++++++++++ 2 files changed, 52 insertions(+) create mode 100644 static/html/register.html create mode 100644 static/js/register.js diff --git a/static/html/register.html b/static/html/register.html new file mode 100644 index 0000000..a916f9d --- /dev/null +++ b/static/html/register.html @@ -0,0 +1,41 @@ + + + + + + + + + + Register + + +
+

Créer un nouveau compte

+

+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+

Tu as déjà un compte?

+ Se connecter. +
+ + + diff --git a/static/js/register.js b/static/js/register.js new file mode 100644 index 0000000..176866a --- /dev/null +++ b/static/js/register.js @@ -0,0 +1,11 @@ +async function register() {} + +function check_username() {} + +function check_password() {} + +let button = document.getElementById("register"); + +button.addEventListener("click", () => { + let _ = register(); +}) \ No newline at end of file -- 2.47.3 From 4101e824a2e6cd2c8c01212bf46dda727073d695 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 28 Sep 2024 23:25:47 +0200 Subject: [PATCH 07/28] permissions mustn't be null --- migrations/20240921193253_create_auth_table.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20240921193253_create_auth_table.sql b/migrations/20240921193253_create_auth_table.sql index d0d0d70..cb4e08f 100644 --- a/migrations/20240921193253_create_auth_table.sql +++ b/migrations/20240921193253_create_auth_table.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS users id INTEGER PRIMARY KEY NOT NULL, username TEXT NOT NULL, saltyhash text NOT NULL, -permissions INTEGER DEFAULT 0, +permissions INTEGER DEFAULT 0 NOT NULL, token TEXT NOT NULL, UNIQUE(id, username) ) \ No newline at end of file -- 2.47.3 From febd1d33f1d3aba9438ed60c4e2504fda3c3d3ff Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 28 Sep 2024 23:26:15 +0200 Subject: [PATCH 08/28] login link --- static/css/index.css | 8 ++++++++ static/css/results.css | 8 ++++++++ static/html/index.html | 1 + static/html/results.html | 1 + static/js/index.js | 7 ++++++- static/js/results.js | 5 +++++ 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/static/css/index.css b/static/css/index.css index 16ebf2a..0ddc9b7 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -119,3 +119,11 @@ body { border: none; border-radius: 5px; } + +.login { + position: fixed; + right: 0; + top: 0; + color: white; + padding: 10px; +} diff --git a/static/css/results.css b/static/css/results.css index a232ff0..0c1f07d 100644 --- a/static/css/results.css +++ b/static/css/results.css @@ -76,3 +76,11 @@ body { grid-column-start: 2; grid-column-end: 3; } + +.login { + position: fixed; + right: 0; + top: 0; + color: white; + padding: 10px; +} diff --git a/static/html/index.html b/static/html/index.html index 8cec0cd..d6482c2 100644 --- a/static/html/index.html +++ b/static/html/index.html @@ -11,6 +11,7 @@ +

Vote +

diff --git a/static/html/results.html b/static/html/results.html index 36da9d7..113e697 100644 --- a/static/html/results.html +++ b/static/html/results.html @@ -11,6 +11,7 @@ +

Tu as déjà un compte?

diff --git a/static/js/register.js b/static/js/register.js index 176866a..1da754c 100644 --- a/static/js/register.js +++ b/static/js/register.js @@ -1,11 +1,58 @@ -async function register() {} +async function register() { + let username = document.getElementById("username").value; + let password = document.getElementById("password").value; + let passwordConfirm = document.getElementById("passwordConfirm").value; + if (!check_username(username) || !check_password(password) || password !== passwordConfirm) { + return; + } + let status = await fetch("/register", {method: "POST", body: JSON.stringify({"username": username, "password": password})}).then(r => r.status) + if (status === 200) { + window.location.href = "/login"; + } else if (status === 400) { + document.getElementById("message").textContent = "Cet utilisateur existe déjà!" + } +} -function check_username() {} +function check_username(username) { + return username.match(/^[0-9a-zA-Z]+$/); +} -function check_password() {} +function check_password(password) { + return password.match(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!-/:-@[-`{-~]).{8,}$/) +} let button = document.getElementById("register"); button.addEventListener("click", () => { let _ = register(); -}) \ No newline at end of file +}) + +let username = document.getElementById("username"); + +username.addEventListener("input", () => { + if (check_username(username.value)) { + document.getElementById("usernameNotice").className = "ok"; + } else { + document.getElementById("usernameNotice").className = "error"; + } +}) + +let password = document.getElementById("password"); + +password.addEventListener("input", () => { + if (check_password(password.value)) { + document.getElementById("passwordNotice").className = "ok"; + } else { + document.getElementById("passwordNotice").className = "error"; + } +}) + +let passwordConfirm = document.getElementById("passwordConfirm"); + +passwordConfirm.addEventListener("input", () => { + if (passwordConfirm.value === password.value) { + document.getElementById("passwordConfirmNotice").className = "ok"; + } else { + document.getElementById("passwordConfirmNotice").className = "error"; + } +}) -- 2.47.3 From c66e854e037138c923ebe022044d34bfdd1d6a35 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 28 Sep 2024 23:27:55 +0200 Subject: [PATCH 13/28] main code + settings update --- routes.json | 17 +++++----- settings.json | 2 +- src/main.rs | 86 +++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/routes.json b/routes.json index a7d69df..d6b6395 100644 --- a/routes.json +++ b/routes.json @@ -1,8 +1,11 @@ { - "/": "static/html/index.html", - "/results": "static/html/results.html", - "/archives": "static/html/archives.html", - "/favicon.ico": "static/img/favicon.ico", - "/login": "static/html/login.html", - "/register": "static/html/register.html" -} + "/": {"file": "static/html/index.html", "permission": 0}, + "/results": {"file": "static/html/results.html", "permission": 1}, + "/archives": {"file": "static/html/archives.html", "permission": 2}, + "/favicon.ico": {"file": "static/img/favicon.ico", "permission": 0}, + "/login": {"file": "static/html/login.html", "permission": 0}, + "/register": {"file": "static/html/register.html", "permission": 0}, + "/logout": {"file": "static/html/logout.html", "permission": 0}, + "/unauthorised": {"file": "static/html/unauthorised.html", "permission": 0}, + "/admin" : {"file": "static/html/admin.html", "permission": 3} +} \ No newline at end of file diff --git a/settings.json b/settings.json index 1cf14dd..70b86c4 100644 --- a/settings.json +++ b/settings.json @@ -1,4 +1,4 @@ { "database_url": "sqlite:vote.db", - "bind_address": "127.0.0.1:8080" + "bind_address": "127.0.0.1:8000" } diff --git a/src/main.rs b/src/main.rs index c6e9376..58240a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use daemonize::Daemonize; use futures; use http_body_util::{BodyExt, Full}; use hyper::body::{Body, Incoming}; -use hyper::header::{LOCATION, SET_COOKIE}; +use hyper::header::{LOCATION, SET_COOKIE, COOKIE}; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Error, Method, Request, Response, StatusCode}; @@ -30,6 +30,7 @@ use std::net::SocketAddr; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::SystemTime; +use hyper::http::HeaderValue; use tokio::net::TcpListener; #[derive(Serialize, Deserialize)] struct Player { @@ -79,20 +80,27 @@ async fn get(req: Request, db: Arc>) -> Result Result>, Error> { +async fn get_page(req: &Request, path: &str, db: Arc>) -> Result>, Error> { let mut routes = env::current_dir().expect("Could not get app directory (Required to get routes)"); routes.push("routes.json"); let file = File::open(routes).expect("Could not open routes file."); let map: Value = from_reader(file).expect("Could not parse routes, please verify syntax."); - match &map[path] { - Value::String(s) => get_file(&s).await, + match map.get(path) { + Some(Value::Object(s)) => { + let perm = is_authorised(req, db).await; + if s.get("permission").unwrap().as_i64().unwrap() <= perm { + get_file(s.get("file").unwrap().as_str().unwrap()).await + } else { + get_file(map.get("/unauthorised").unwrap().get("file").unwrap().as_str().unwrap()).await + } + }, _ => not_found().await } } @@ -185,10 +193,8 @@ async fn get_votes(req: &Request, db: Arc>) -> Vec, db: Arc>) -> Vec, path: &str, db: Arc>) -> Result>, Error> { + let perm = is_authorised(req, db.clone()).await; + if perm < 3 { + return not_found().await; + } + if path == "/admin" { + return get_page(req, path, db).await; + } + if path == "/admin/users" { + let pool = db.clone().lock().unwrap().clone(); + let users = sqlx::query!(r#"SELECT username, permissions FROM users"#).fetch_all(&pool).await.unwrap(); + let users: Vec<(String, i64)> = users.iter().map(|x| (x.username.clone(), x.permissions)).collect(); + let stringed = serde_json::to_string(&users).unwrap_or("".to_string()); + return Ok(Response::builder().body(Full::new(Bytes::from(stringed))).unwrap()); + } + not_found().await +} + async fn post(req: Request, db: Arc>) -> Result>, Error> { let path = req.uri().path(); match path { @@ -211,6 +235,9 @@ async fn post(req: Request, db: Arc>) -> Result { register(req, db).await } + "/logout" => { + logout().await + } _ => { not_found().await } @@ -254,8 +281,7 @@ async fn login(req: Request, db: Arc>) -> Result { let argon = Argon2::default(); @@ -268,6 +294,7 @@ async fn login(req: Request, db: Arc>) -> Result, db: Arc>) -> Result< let argon2 = Argon2::default(); let hash = argon2.hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng)).unwrap().to_string(); let token = Alphanumeric.sample_string(&mut OsRng, 256); - println!("{}", token); let result = sqlx::query!(r#"INSERT INTO users ( username, saltyhash, permissions, token) VALUES ( ?1, ?2, ?3, ?4 )"#, data.username, hash, 0, token).execute(&mut *conn).await; match result { Ok(_) => Ok(Response::builder().body(Full::new(Bytes::from(""))).unwrap()), @@ -316,12 +342,40 @@ async fn register(req: Request, db: Arc>) -> Result< async fn logout() -> Result>, Error> { let date: DateTime = DateTime::from(SystemTime::now()); Ok(Response::builder() - .status(StatusCode::SEE_OTHER) - .header(LOCATION, "/") + //.status(StatusCode::SEE_OTHER) + //.header(LOCATION, "/") .header(SET_COOKIE, format!("token=''; Expires={}; Secure; HttpOnly; SameSite=Strict", date.to_rfc2822())) + .header(SET_COOKIE, format!("logged=false; Expires={}; Secure; HttpOnly; SameSite=Strict", date.to_rfc2822())) .body(Full::new(Bytes::from(""))).unwrap()) } +async fn is_authorised(req: &Request, db: Arc>) -> i64 { + let cookies = req.headers().get(COOKIE); + let token = match cookies { + Some(cookies) => { + cookies + .to_str() + .unwrap_or("") + .split("; ") + .find(|x| x.starts_with("token=")) + .unwrap_or("") + .strip_prefix("token=") + .unwrap_or("")}, + None => "" + }; + let pool = db.clone().lock().unwrap().clone(); + let user = sqlx::query!(r#"SELECT permissions FROM users WHERE token=?1"#, token).fetch_optional(&pool).await; + match user { + Ok(user) => { + match user { + Some(user) => user.permissions, + None => 0 + } + }, + Err(_) => 0 + } +} + fn check_username(username: &String) -> bool { if username.len() > 21 { return false; -- 2.47.3 From 4cb1ef1df9c6e676333f5a1786d72f1cec4db8e5 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 28 Sep 2024 23:28:14 +0200 Subject: [PATCH 14/28] admin page (in progress) --- static/html/admin.html | 22 ++++++++++++++++++++++ static/js/admin.js | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 static/html/admin.html create mode 100644 static/js/admin.js diff --git a/static/html/admin.html b/static/html/admin.html new file mode 100644 index 0000000..f5db6f6 --- /dev/null +++ b/static/html/admin.html @@ -0,0 +1,22 @@ + + + + + + + + + Administration + + +
+

Users

+
+
+
+

Votes

+
+
+ + \ No newline at end of file diff --git a/static/js/admin.js b/static/js/admin.js new file mode 100644 index 0000000..420a85a --- /dev/null +++ b/static/js/admin.js @@ -0,0 +1,20 @@ +async function run() { + let users = await fetch("/admin/users").then(r => r.json()); + + let usersDiv = document.getElementById("users"); + for (let i = 0; i < users.length; i++) { + let item = document.createElement("div"); + let username = document.createElement("p"); + let permissions = document.createElement("p"); + username.textContent = users[i][0]; + permissions.textContent = users[i][1]; + item.style.display = "flex"; + item.append(username, permissions); + usersDiv.appendChild(item); + } + +// let votes = await fetch("/admin/votes").then(r => r.json()); + +} + +let _ = run(); \ No newline at end of file -- 2.47.3 From 72fd45eb64622cae250bbe65458f7545c1ab5381 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Tue, 1 Oct 2024 21:27:23 +0200 Subject: [PATCH 15/28] admin page (in progress) + better body (in progress) --- src/main.rs | 178 +++++++++++++++++++++++++++++---------------- static/js/admin.js | 12 +-- 2 files changed, 122 insertions(+), 68 deletions(-) diff --git a/src/main.rs b/src/main.rs index 58240a9..1f1f100 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,8 +11,8 @@ use chrono::{DateTime, Days, Utc}; #[cfg(target_os = "linux")] use daemonize::Daemonize; use futures; -use http_body_util::{BodyExt, Full}; -use hyper::body::{Body, Incoming}; +use http_body_util::{BodyExt, Empty, Full}; +use hyper::body::{Body, Frame, Incoming, SizeHint}; use hyper::header::{LOCATION, SET_COOKIE, COOKIE}; use hyper::server::conn::http1; use hyper::service::service_fn; @@ -23,15 +23,41 @@ use serde::{Deserialize, Serialize}; use serde_json::{from_reader, Value}; use sqlx::sqlite::SqlitePool; use std::collections::HashMap; +use std::convert::Infallible; use std::env; use std::fs::File; use std::io::Read; use std::net::SocketAddr; +use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; use std::time::SystemTime; use hyper::http::HeaderValue; +use serde::de::DeserializeOwned; use tokio::net::TcpListener; + +// Some functions could return an empty body, will try using the following enum: +enum ResponseBody { + Full(Full), + Empty, +} + +impl Body for ResponseBody { + type Data = Bytes; + type Error = Infallible; + fn poll_frame(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll, Self::Error>>> { + match &mut *self.get_mut() { + Self::Full(incoming) => { + Pin::new(incoming).poll_frame(cx) + }, + Self::Empty => { + Poll::Ready(None) + } + } + } +} + #[derive(Serialize, Deserialize)] struct Player { id: i64, @@ -66,15 +92,16 @@ struct Settings { database_url: String, bind_address: String, } -async fn service(req: Request, db: Arc>) -> Result>, Error> { + +async fn service(req: Request, db: Arc>) -> Result, Error> { match req.method() { &Method::GET => { get(req, db).await } &Method::POST => { post(req, db).await } - _ => { Ok(Response::builder().status(StatusCode::IM_A_TEAPOT).body(Full::new(Bytes::new())).unwrap()) } + _ => { Ok(Response::builder().status(StatusCode::IM_A_TEAPOT).body(ResponseBody::Empty).unwrap()) } } } -async fn get(req: Request, db: Arc>) -> Result>, Error> { +async fn get(req: Request, db: Arc>) -> Result, Error> { let path = req.uri().path(); if path.starts_with("/static") { get_file(path).await @@ -87,7 +114,7 @@ async fn get(req: Request, db: Arc>) -> Result, path: &str, db: Arc>) -> Result>, Error> { +async fn get_page(req: &Request, path: &str, db: Arc>) -> Result, Error> { let mut routes = env::current_dir().expect("Could not get app directory (Required to get routes)"); routes.push("routes.json"); let file = File::open(routes).expect("Could not open routes file."); @@ -105,7 +132,7 @@ async fn get_page(req: &Request, path: &str, db: Arc } } -async fn get_file(mut path: &str) -> Result>, Error> { +async fn get_file(mut path: &str) -> Result, Error> { let mut file_path = env::current_dir().expect("Could not get app directory."); if path.starts_with(r"/") { path = path.strip_prefix(r"/").unwrap(); @@ -122,23 +149,23 @@ async fn get_file(mut path: &str) -> Result>, Error> { "css" => "text/css", _ => "" }; - Ok(Response::builder().header("content-type", content_type).body(Full::new(Bytes::from(buf))).unwrap()) + Ok(Response::builder().header("content-type", content_type).body(ResponseBody::Full(Full::new(Bytes::from(buf)))).unwrap()) } Err(_) => not_found().await } } -async fn get_data(path: &str, req: &Request, db: Arc>) -> Result>, Error> { +async fn get_data(path: &str, req: &Request, db: Arc>) -> Result, Error> { let pool = db.clone().lock().unwrap().clone(); match path { "/data/players" => { let items = sqlx::query!(r#"SELECT * FROM players"#).fetch_all(&pool).await.unwrap(); let players: Vec = items.iter().map(|x| Player { id: x.id, name: x.name.clone() }).collect(); - Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&players).unwrap())))) + Ok(Response::new(ResponseBody::Full(Full::new(Bytes::from(serde_json::to_string(&players).unwrap()))))) } "/data/votes" => { let votes = get_votes(req, db).await; - Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&votes).unwrap())))) + Ok(Response::new(ResponseBody::Full(Full::new(Bytes::from(serde_json::to_string(&votes).unwrap()))))) } "/data/results" => { let votes = get_votes(req, db).await; @@ -169,7 +196,7 @@ async fn get_data(path: &str, req: &Request, db: Arc let sorted_results = vec![plus_results, minus_results]; - Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&sorted_results).unwrap())))) + Ok(Response::new(ResponseBody::Full(Full::new(Bytes::from(serde_json::to_string(&sorted_results).unwrap()))))) } _ => not_found().await } @@ -205,7 +232,7 @@ async fn get_votes(req: &Request, db: Arc>) -> Vec, path: &str, db: Arc>) -> Result>, Error> { +async fn get_admin(req: &Request, path: &str, db: Arc>) -> Result, Error> { let perm = is_authorised(req, db.clone()).await; if perm < 3 { return not_found().await; @@ -218,13 +245,16 @@ async fn get_admin(req: &Request, path: &str, db: Arc = users.iter().map(|x| (x.username.clone(), x.permissions)).collect(); let stringed = serde_json::to_string(&users).unwrap_or("".to_string()); - return Ok(Response::builder().body(Full::new(Bytes::from(stringed))).unwrap()); + return Ok(Response::builder().body(ResponseBody::Full(Full::new(Bytes::from(stringed)))).unwrap()); } not_found().await } -async fn post(req: Request, db: Arc>) -> Result>, Error> { +async fn post(req: Request, db: Arc>) -> Result, Error> { let path = req.uri().path(); + if path.starts_with("/admin") { + return post_admin(req, db).await; + } match path { "/vote" => { post_vote(req, db).await @@ -244,11 +274,11 @@ async fn post(req: Request, db: Arc>) -> Result, db: Arc>) -> Result>, Error> { +async fn post_vote(req: Request, db: Arc>) -> Result, Error> { let body = req.into_body().collect().await?; let data: Result = from_reader(body.aggregate().reader()); if data.is_err() { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()); } let vote = data.unwrap(); let timestamp: DateTime = DateTime::from(SystemTime::now()); @@ -265,20 +295,37 @@ async fn post_vote(req: Request, db: Arc>) -> Result vote.minus_reason, formatted).execute(&mut *conn).await; if result.is_err() { - return Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Internet Error"))).unwrap()); + return Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(ResponseBody::Empty).unwrap()); } - Ok(Response::builder().body(Full::new(Bytes::new())).unwrap()) + Ok(Response::builder().body(ResponseBody::Full(Full::new(Bytes::new()))).unwrap()) } -async fn login(req: Request, db: Arc>) -> Result>, Error> { +async fn post_admin(req: Request, db: Arc>) -> Result, Error> { + let perm = is_authorised(&req, db.clone()).await; + if perm < 3 { + return get_page(&req, "/unauthorised", db).await; + } + let path = req.uri().path(); + match path { + "/admin/post/user" => { + req_json::(req).await; + }, + "/admin/post/vote" => {}, + "/admin/post/player" => {}, + _ => {} + } + not_found().await +} + +async fn login(req: Request, db: Arc>) -> Result, Error> { let body = req.into_body().collect().await; let data: Result = from_reader(body?.aggregate().reader()); if data.is_err() { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()); } let data = data.unwrap(); if !check_username(&data.username) { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Full(Full::new(Bytes::from("Bad Request")))).unwrap()); } let pool = db.clone().lock().unwrap().clone(); let result = sqlx::query!(r#"SELECT * FROM users WHERE username=?1"#, data.username).fetch_optional(&pool).await; @@ -295,58 +342,57 @@ async fn login(req: Request, db: Arc>) -> Result { - Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) + Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()) } } } Ok(None) => { - Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) + Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()) } - Err(_) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Server Error"))).unwrap()) } + Err(_) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(ResponseBody::Empty).unwrap()) } } } -async fn register(req: Request, db: Arc>) -> Result>, Error> { - let body = req.into_body().collect().await; - let data: Result = from_reader(body?.aggregate().reader()); - if data.is_err() { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); - } - let data = data.unwrap(); - if !check_username(&data.username) { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); - } - if !check_password(&data.password) { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); - } - let pool = db.clone().lock().unwrap().clone(); - let mut conn = pool.acquire().await.unwrap(); - let exists = sqlx::query!(r#"SELECT id FROM users WHERE username=?1"#, data.username).fetch_optional(&mut *conn).await; - if exists.unwrap().is_some() { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); - } - let argon2 = Argon2::default(); - let hash = argon2.hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng)).unwrap().to_string(); - let token = Alphanumeric.sample_string(&mut OsRng, 256); - let result = sqlx::query!(r#"INSERT INTO users ( username, saltyhash, permissions, token) VALUES ( ?1, ?2, ?3, ?4 )"#, data.username, hash, 0, token).execute(&mut *conn).await; - match result { - Ok(_) => Ok(Response::builder().body(Full::new(Bytes::from(""))).unwrap()), - Err(_) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Server Error"))).unwrap()) } +async fn register(req: Request, db: Arc>) -> Result, Error> { + match req_json::(req).await { + None => Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()), + Some(login) => { + if !check_username(&login.username) { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()); + } + if !check_password(&login.password) { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()); + } + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let exists = sqlx::query!(r#"SELECT id FROM users WHERE username=?1"#, login.username).fetch_optional(&mut *conn).await; + if exists.unwrap().is_some() { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()); + } + let argon2 = Argon2::default(); + let hash = argon2.hash_password(login.password.as_bytes(), &SaltString::generate(&mut OsRng)).unwrap().to_string(); + let token = Alphanumeric.sample_string(&mut OsRng, 256); + let result = sqlx::query!(r#"INSERT INTO users ( username, saltyhash, permissions, token) VALUES ( ?1, ?2, ?3, ?4 )"#, login.username, hash, 0, token).execute(&mut *conn).await; + match result { + Ok(_) => Ok(Response::builder().body(ResponseBody::Full(Full::new(Bytes::new()))).unwrap()), + Err(_) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(ResponseBody::Empty).unwrap()) } + } + }, } } -async fn logout() -> Result>, Error> { +async fn logout() -> Result, Error> { let date: DateTime = DateTime::from(SystemTime::now()); Ok(Response::builder() //.status(StatusCode::SEE_OTHER) //.header(LOCATION, "/") .header(SET_COOKIE, format!("token=''; Expires={}; Secure; HttpOnly; SameSite=Strict", date.to_rfc2822())) .header(SET_COOKIE, format!("logged=false; Expires={}; Secure; HttpOnly; SameSite=Strict", date.to_rfc2822())) - .body(Full::new(Bytes::from(""))).unwrap()) + .body(ResponseBody::Empty).unwrap()) } async fn is_authorised(req: &Request, db: Arc>) -> i64 { @@ -366,13 +412,8 @@ async fn is_authorised(req: &Request, db: Arc>) -> i let pool = db.clone().lock().unwrap().clone(); let user = sqlx::query!(r#"SELECT permissions FROM users WHERE token=?1"#, token).fetch_optional(&pool).await; match user { - Ok(user) => { - match user { - Some(user) => user.permissions, - None => 0 - } - }, - Err(_) => 0 + Ok(Some(user)) => user.permissions, + _ => 0 } } @@ -410,13 +451,24 @@ fn check_password(password: &String) -> bool { up && num && sym } -async fn not_found() -> Result>, Error> { +async fn not_found() -> Result, Error> { let mut file_path = env::current_dir().expect("Could not get app directory."); file_path.push("static/html/404.html"); let mut file = File::open(file_path).unwrap(); let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); - Ok(Response::builder().status(StatusCode::NOT_FOUND).body(Full::new(Bytes::from(buf))).unwrap()) + Ok(Response::builder().status(StatusCode::NOT_FOUND).body(ResponseBody::Full(Full::new(Bytes::from(buf)))).unwrap()) +} + +async fn req_json(req: Request) -> Option +where + T: DeserializeOwned +{ + let body = req.into_body().collect().await.unwrap(); + match from_reader(body.aggregate().reader()) { + Ok(val) => Some(val), + Err(_) => None, + } } fn get_settings() -> Settings { diff --git a/static/js/admin.js b/static/js/admin.js index 420a85a..02117b8 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -4,12 +4,14 @@ async function run() { let usersDiv = document.getElementById("users"); for (let i = 0; i < users.length; i++) { let item = document.createElement("div"); - let username = document.createElement("p"); - let permissions = document.createElement("p"); - username.textContent = users[i][0]; - permissions.textContent = users[i][1]; + let username = document.createElement("input"); + let permissions = document.createElement("input"); + let edit = document.createElement("button"); + edit.textContent = "Edit"; + username.value = users[i][0]; + permissions.value = users[i][1]; item.style.display = "flex"; - item.append(username, permissions); + item.append(username, permissions, edit); usersDiv.appendChild(item); } -- 2.47.3 From 894e102322419055a86037fc29d11ee40c9b1a5f Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Wed, 2 Oct 2024 19:36:50 +0200 Subject: [PATCH 16/28] Custom Response Body, is authorised returning bool, ... --- src/main.rs | 472 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 310 insertions(+), 162 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1f1f100..afcf5fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,22 @@ use argon2::{ - password_hash::{ - rand_core::OsRng, - PasswordHash, PasswordHasher, PasswordVerifier, SaltString, - }, + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; -use bytes::Buf; -use bytes::Bytes; +use bytes::{Buf, Bytes}; use chrono::{DateTime, Days, Utc}; #[cfg(target_os = "linux")] use daemonize::Daemonize; -use futures; -use http_body_util::{BodyExt, Empty, Full}; -use hyper::body::{Body, Frame, Incoming, SizeHint}; -use hyper::header::{LOCATION, SET_COOKIE, COOKIE}; -use hyper::server::conn::http1; -use hyper::service::service_fn; -use hyper::{Error, Method, Request, Response, StatusCode}; +use http_body_util::{BodyExt, Full}; +use hyper::{ + body::{Body as HyperBody, Incoming, Frame}, + header::{COOKIE, SET_COOKIE}, + server::conn::http1, + service::service_fn, + Error, Method, Request, Response, StatusCode, +}; use hyper_util::rt::{TokioIo, TokioTimer}; use rand::distributions::{Alphanumeric, DistString}; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::{from_reader, Value}; use sqlx::sqlite::SqlitePool; use std::collections::HashMap; @@ -33,27 +30,32 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; use std::time::SystemTime; -use hyper::http::HeaderValue; -use serde::de::DeserializeOwned; use tokio::net::TcpListener; -// Some functions could return an empty body, will try using the following enum: -enum ResponseBody { +enum Body { Full(Full), Empty, } -impl Body for ResponseBody { +impl Body { + fn new(data: T) -> Self + where + Bytes: From, + { + Body::Full(Full::new(Bytes::from(data))) + } +} + +impl HyperBody for Body { type Data = Bytes; type Error = Infallible; - fn poll_frame(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll, Self::Error>>> { + fn poll_frame( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { match &mut *self.get_mut() { - Self::Full(incoming) => { - Pin::new(incoming).poll_frame(cx) - }, - Self::Empty => { - Poll::Ready(None) - } + Self::Full(incoming) => Pin::new(incoming).poll_frame(cx), + Self::Empty => Poll::Ready(None), } } } @@ -93,15 +95,21 @@ struct Settings { bind_address: String, } -async fn service(req: Request, db: Arc>) -> Result, Error> { +async fn service( + req: Request, + db: Arc>, +) -> Result, Error> { match req.method() { - &Method::GET => { get(req, db).await } - &Method::POST => { post(req, db).await } - _ => { Ok(Response::builder().status(StatusCode::IM_A_TEAPOT).body(ResponseBody::Empty).unwrap()) } + &Method::GET => get(req, db).await, + &Method::POST => post(req, db).await, + _ => Ok(Response::builder() + .status(StatusCode::IM_A_TEAPOT) + .body(Body::Empty) + .unwrap()), } } -async fn get(req: Request, db: Arc>) -> Result, Error> { +async fn get(req: Request, db: Arc>) -> Result, Error> { let path = req.uri().path(); if path.starts_with("/static") { get_file(path).await @@ -114,25 +122,38 @@ async fn get(req: Request, db: Arc>) -> Result, path: &str, db: Arc>) -> Result, Error> { - let mut routes = env::current_dir().expect("Could not get app directory (Required to get routes)"); +async fn get_page( + req: &Request, + path: &str, + db: Arc>, +) -> Result, Error> { + let mut routes = + env::current_dir().expect("Could not get app directory (Required to get routes)"); routes.push("routes.json"); let file = File::open(routes).expect("Could not open routes file."); let map: Value = from_reader(file).expect("Could not parse routes, please verify syntax."); match map.get(path) { Some(Value::Object(s)) => { - let perm = is_authorised(req, db).await; - if s.get("permission").unwrap().as_i64().unwrap() <= perm { + let authorised = is_authorised(req, db, s.get("permission").unwrap().as_u64().unwrap() as u8).await; + if authorised { get_file(s.get("file").unwrap().as_str().unwrap()).await } else { - get_file(map.get("/unauthorised").unwrap().get("file").unwrap().as_str().unwrap()).await + get_file( + map.get("/unauthorised") + .unwrap() + .get("file") + .unwrap() + .as_str() + .unwrap(), + ) + .await } - }, - _ => not_found().await + } + _ => not_found().await, } } -async fn get_file(mut path: &str) -> Result, Error> { +async fn get_file(mut path: &str) -> Result, Error> { let mut file_path = env::current_dir().expect("Could not get app directory."); if path.starts_with(r"/") { path = path.strip_prefix(r"/").unwrap(); @@ -147,29 +168,52 @@ async fn get_file(mut path: &str) -> Result, Error> { "js" => "text/javascript", "html" => "text/html", "css" => "text/css", - _ => "" + _ => "", }; - Ok(Response::builder().header("content-type", content_type).body(ResponseBody::Full(Full::new(Bytes::from(buf)))).unwrap()) + Ok(Response::builder() + .header("content-type", content_type) + .body(Body::new(buf)) + .unwrap()) } - Err(_) => not_found().await + Err(_) => not_found().await, } } -async fn get_data(path: &str, req: &Request, db: Arc>) -> Result, Error> { +async fn get_data( + path: &str, + req: &Request, + db: Arc>, +) -> Result, Error> { let pool = db.clone().lock().unwrap().clone(); match path { "/data/players" => { - let items = sqlx::query!(r#"SELECT * FROM players"#).fetch_all(&pool).await.unwrap(); - let players: Vec = items.iter().map(|x| Player { id: x.id, name: x.name.clone() }).collect(); - Ok(Response::new(ResponseBody::Full(Full::new(Bytes::from(serde_json::to_string(&players).unwrap()))))) + let items = sqlx::query!(r#"SELECT * FROM players"#) + .fetch_all(&pool) + .await + .unwrap(); + let players: Vec = items + .iter() + .map(|x| Player { + id: x.id, + name: x.name.clone(), + }) + .collect(); + Ok(Response::new(Body::new( + serde_json::to_string(&players).unwrap(), + ))) } "/data/votes" => { let votes = get_votes(req, db).await; - Ok(Response::new(ResponseBody::Full(Full::new(Bytes::from(serde_json::to_string(&votes).unwrap()))))) + Ok(Response::new(Body::new( + serde_json::to_string(&votes).unwrap(), + ))) } "/data/results" => { let votes = get_votes(req, db).await; - let ids: Vec<(i64, i64)> = votes.iter().map(|x| (x.plus_player_id, x.minus_player_id)).collect(); + let ids: Vec<(i64, i64)> = votes + .iter() + .map(|x| (x.plus_player_id, x.minus_player_id)) + .collect(); let mut plus_results: HashMap = HashMap::new(); let mut minus_results: HashMap = HashMap::new(); @@ -191,14 +235,16 @@ async fn get_data(path: &str, req: &Request, db: Arc let mut plus_results: Vec<(i64, i64)> = plus_results.into_iter().collect(); let mut minus_results: Vec<(i64, i64)> = minus_results.into_iter().collect(); - plus_results.sort_by(|a, b| { b.1.cmp(&a.1) }); - minus_results.sort_by(|a, b| { b.1.cmp(&a.1) }); + plus_results.sort_by(|a, b| b.1.cmp(&a.1)); + minus_results.sort_by(|a, b| b.1.cmp(&a.1)); let sorted_results = vec![plus_results, minus_results]; - Ok(Response::new(ResponseBody::Full(Full::new(Bytes::from(serde_json::to_string(&sorted_results).unwrap()))))) + Ok(Response::new(Body::new( + serde_json::to_string(&sorted_results).unwrap(), + ))) } - _ => not_found().await + _ => not_found().await, } } @@ -215,26 +261,39 @@ async fn get_votes(req: &Request, db: Arc>) -> Vec Some(DateTime::from(SystemTime::now())) + None => Some(DateTime::from(SystemTime::now())), }; if date.is_none() { return Vec::new(); } let formatted_date = format!("{}", date.unwrap().format("%d/%m/%Y")); - let items = sqlx::query!(r#"SELECT * FROM votes WHERE submit_date = ?1 ORDER BY id"#, formatted_date).fetch_all(&pool).await.unwrap(); - items.iter().map(|x| Vote { - plus_player_id: x.plus_player_id, - plus_nickname: x.plus_nickname.clone(), - plus_reason: x.plus_reason.clone(), - minus_player_id: x.minus_player_id, - minus_nickname: x.minus_nickname.clone(), - minus_reason: x.minus_reason.clone(), - }).collect() + let items = sqlx::query!( + r#"SELECT * FROM votes WHERE submit_date = ?1 ORDER BY id"#, + formatted_date + ) + .fetch_all(&pool) + .await + .unwrap(); + items + .iter() + .map(|x| Vote { + plus_player_id: x.plus_player_id, + plus_nickname: x.plus_nickname.clone(), + plus_reason: x.plus_reason.clone(), + minus_player_id: x.minus_player_id, + minus_nickname: x.minus_nickname.clone(), + minus_reason: x.minus_reason.clone(), + }) + .collect() } -async fn get_admin(req: &Request, path: &str, db: Arc>) -> Result, Error> { - let perm = is_authorised(req, db.clone()).await; - if perm < 3 { +async fn get_admin( + req: &Request, + path: &str, + db: Arc>, +) -> Result, Error> { + let authorised = is_authorised(req, db.clone(), 3).await; + if authorised { return not_found().await; } if path == "/admin" { @@ -242,43 +301,45 @@ async fn get_admin(req: &Request, path: &str, db: Arc = users.iter().map(|x| (x.username.clone(), x.permissions)).collect(); + let users = sqlx::query!(r#"SELECT username, permissions FROM users"#) + .fetch_all(&pool) + .await + .unwrap(); + let users: Vec<(String, i64)> = users + .iter() + .map(|x| (x.username.clone(), x.permissions)) + .collect(); let stringed = serde_json::to_string(&users).unwrap_or("".to_string()); - return Ok(Response::builder().body(ResponseBody::Full(Full::new(Bytes::from(stringed)))).unwrap()); + return Ok(Response::builder().body(Body::new(stringed)).unwrap()); } not_found().await } -async fn post(req: Request, db: Arc>) -> Result, Error> { +async fn post(req: Request, db: Arc>) -> Result, Error> { let path = req.uri().path(); if path.starts_with("/admin") { return post_admin(req, db).await; } match path { - "/vote" => { - post_vote(req, db).await - } - "/login" => { - login(req, db).await - } - "/register" => { - register(req, db).await - } - "/logout" => { - logout().await - } - _ => { - not_found().await - } + "/vote" => post_vote(req, db).await, + "/login" => login(req, db).await, + "/register" => register(req, db).await, + "/logout" => logout().await, + _ => not_found().await, } } -async fn post_vote(req: Request, db: Arc>) -> Result, Error> { +async fn post_vote( + req: Request, + db: Arc>, +) -> Result, Error> { let body = req.into_body().collect().await?; let data: Result = from_reader(body.aggregate().reader()); if data.is_err() { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()); + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::Empty) + .unwrap()); } let vote = data.unwrap(); let timestamp: DateTime = DateTime::from(SystemTime::now()); @@ -295,40 +356,57 @@ async fn post_vote(req: Request, db: Arc>) -> Result vote.minus_reason, formatted).execute(&mut *conn).await; if result.is_err() { - return Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(ResponseBody::Empty).unwrap()); + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::Empty) + .unwrap()); } - Ok(Response::builder().body(ResponseBody::Full(Full::new(Bytes::new()))).unwrap()) + Ok(Response::builder().body(Body::Empty).unwrap()) } -async fn post_admin(req: Request, db: Arc>) -> Result, Error> { - let perm = is_authorised(&req, db.clone()).await; - if perm < 3 { +async fn post_admin( + req: Request, + db: Arc>, +) -> Result, Error> { + let authorised = is_authorised(&req, db.clone(), 3).await; + if authorised { return get_page(&req, "/unauthorised", db).await; } let path = req.uri().path(); match path { "/admin/post/user" => { req_json::(req).await; - }, - "/admin/post/vote" => {}, - "/admin/post/player" => {}, + } + "/admin/post/vote" => {} + "/admin/post/player" => {} _ => {} } not_found().await } -async fn login(req: Request, db: Arc>) -> Result, Error> { +async fn login( + req: Request, + db: Arc>, +) -> Result, Error> { let body = req.into_body().collect().await; let data: Result = from_reader(body?.aggregate().reader()); if data.is_err() { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()); + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::Empty) + .unwrap()); } let data = data.unwrap(); if !check_username(&data.username) { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Full(Full::new(Bytes::from("Bad Request")))).unwrap()); + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::Empty) + .unwrap()); } let pool = db.clone().lock().unwrap().clone(); - let result = sqlx::query!(r#"SELECT * FROM users WHERE username=?1"#, data.username).fetch_optional(&pool).await; + let result = sqlx::query!(r#"SELECT * FROM users WHERE username=?1"#, data.username) + .fetch_optional(&pool) + .await; match result { Ok(Some(user)) => { let argon = Argon2::default(); @@ -339,81 +417,139 @@ async fn login(req: Request, db: Arc>) -> Result { - Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()) - } + Err(_) => Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::Empty) + .unwrap()), } } - Ok(None) => { - Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()) - } - Err(_) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(ResponseBody::Empty).unwrap()) } + Ok(None) => Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::Empty) + .unwrap()), + Err(_) => Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::Empty) + .unwrap()), } } -async fn register(req: Request, db: Arc>) -> Result, Error> { +async fn register( + req: Request, + db: Arc>, +) -> Result, Error> { match req_json::(req).await { - None => Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()), + None => Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::Empty) + .unwrap()), Some(login) => { if !check_username(&login.username) { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()); + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::Empty) + .unwrap()); } if !check_password(&login.password) { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()); + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::Empty) + .unwrap()); } let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); - let exists = sqlx::query!(r#"SELECT id FROM users WHERE username=?1"#, login.username).fetch_optional(&mut *conn).await; + let exists = sqlx::query!(r#"SELECT id FROM users WHERE username=?1"#, login.username) + .fetch_optional(&mut *conn) + .await; if exists.unwrap().is_some() { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(ResponseBody::Empty).unwrap()); + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::Empty) + .unwrap()); } let argon2 = Argon2::default(); - let hash = argon2.hash_password(login.password.as_bytes(), &SaltString::generate(&mut OsRng)).unwrap().to_string(); + let hash = argon2 + .hash_password(login.password.as_bytes(), &SaltString::generate(&mut OsRng)) + .unwrap() + .to_string(); let token = Alphanumeric.sample_string(&mut OsRng, 256); let result = sqlx::query!(r#"INSERT INTO users ( username, saltyhash, permissions, token) VALUES ( ?1, ?2, ?3, ?4 )"#, login.username, hash, 0, token).execute(&mut *conn).await; match result { - Ok(_) => Ok(Response::builder().body(ResponseBody::Full(Full::new(Bytes::new()))).unwrap()), - Err(_) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(ResponseBody::Empty).unwrap()) } + Ok(_) => Ok(Response::builder().body(Body::Empty).unwrap()), + Err(_) => Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::Empty) + .unwrap()), } - }, + } } } -async fn logout() -> Result, Error> { +async fn logout() -> Result, Error> { let date: DateTime = DateTime::from(SystemTime::now()); Ok(Response::builder() - //.status(StatusCode::SEE_OTHER) - //.header(LOCATION, "/") - .header(SET_COOKIE, format!("token=''; Expires={}; Secure; HttpOnly; SameSite=Strict", date.to_rfc2822())) - .header(SET_COOKIE, format!("logged=false; Expires={}; Secure; HttpOnly; SameSite=Strict", date.to_rfc2822())) - .body(ResponseBody::Empty).unwrap()) + .header( + SET_COOKIE, + format!( + "token=''; Expires={}; Secure; HttpOnly; SameSite=Strict", + date.to_rfc2822() + ), + ) + .header( + SET_COOKIE, + format!( + "logged=false; Expires={}; Secure; HttpOnly; SameSite=Strict", + date.to_rfc2822() + ), + ) + .body(Body::Empty) + .unwrap()) } -async fn is_authorised(req: &Request, db: Arc>) -> i64 { +async fn is_authorised(req: &Request, db: Arc>, level: u8) -> bool { let cookies = req.headers().get(COOKIE); let token = match cookies { - Some(cookies) => { - cookies - .to_str() - .unwrap_or("") - .split("; ") - .find(|x| x.starts_with("token=")) - .unwrap_or("") - .strip_prefix("token=") - .unwrap_or("")}, - None => "" + Some(cookies) => cookies + .to_str() + .unwrap_or("") + .split("; ") + .find(|x| x.starts_with("token=")) + .unwrap_or("") + .strip_prefix("token=") + .unwrap_or(""), + None => "", }; let pool = db.clone().lock().unwrap().clone(); - let user = sqlx::query!(r#"SELECT permissions FROM users WHERE token=?1"#, token).fetch_optional(&pool).await; + let user = sqlx::query!(r#"SELECT permissions FROM users WHERE token=?1"#, token) + .fetch_optional(&pool) + .await; match user { - Ok(Some(user)) => user.permissions, - _ => 0 + Ok(Some(user)) => { + let perm = user.permissions as u8; + perm >= level + }, + _ => match level { + 0 => true, + _ => false + }, } } @@ -451,18 +587,21 @@ fn check_password(password: &String) -> bool { up && num && sym } -async fn not_found() -> Result, Error> { +async fn not_found() -> Result, Error> { let mut file_path = env::current_dir().expect("Could not get app directory."); file_path.push("static/html/404.html"); let mut file = File::open(file_path).unwrap(); let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); - Ok(Response::builder().status(StatusCode::NOT_FOUND).body(ResponseBody::Full(Full::new(Bytes::from(buf)))).unwrap()) + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::new(buf)) + .unwrap()) } async fn req_json(req: Request) -> Option where - T: DeserializeOwned + T: DeserializeOwned, { let body = req.into_body().collect().await.unwrap(); match from_reader(body.aggregate().reader()) { @@ -472,10 +611,13 @@ where } fn get_settings() -> Settings { - let mut settings_path = env::current_dir().expect("Could not get app directory. (Required to read settings)"); + let mut settings_path = + env::current_dir().expect("Could not get app directory. (Required to read settings)"); settings_path.push("settings.json"); - let settings_file = File::open(settings_path).expect("Could not open settings file, does it exists?"); - let settings: Settings = from_reader(settings_file).expect("Could not parse settings, please check syntax."); + let settings_file = + File::open(settings_path).expect("Could not open settings file, does it exists?"); + let settings: Settings = + from_reader(settings_file).expect("Could not parse settings, please check syntax."); settings } @@ -504,26 +646,32 @@ fn main() { #[tokio::main] async fn run() { let settings = get_settings(); - let db_pool = Arc::new(Mutex::new(SqlitePool::connect(&settings.database_url).await.expect("Could not connect to database. Make sure the url is correct."))); - let bind_address: SocketAddr = SocketAddr::from_str(&settings.bind_address).expect("Could not parse bind address."); - let listener = TcpListener::bind(bind_address).await.expect("Could not bind to address."); + let db_pool = Arc::new(Mutex::new( + SqlitePool::connect(&settings.database_url) + .await + .expect("Could not connect to database. Make sure the url is correct."), + )); + let bind_address: SocketAddr = + SocketAddr::from_str(&settings.bind_address).expect("Could not parse bind address."); + let listener = TcpListener::bind(bind_address) + .await + .expect("Could not bind to address."); loop { - let (stream, _) = listener.accept().await.expect("Could not accept incoming stream."); + let (stream, _) = listener + .accept() + .await + .expect("Could not accept incoming stream."); let io = TokioIo::new(stream); let db = db_pool.clone(); - let service = service_fn( - move |req| { - service(req, db.clone()) + let service = service_fn(move |req| service(req, db.clone())); + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .timer(TokioTimer::new()) + .serve_connection(io, service) + .await + { + println!("Failed to serve connection: {:?}", err); } - ); - tokio::task::spawn( - async move { - if let Err(err) = http1::Builder::new() - .timer(TokioTimer::new()) - .serve_connection(io, service).await { - println!("Failed to serve connection: {:?}", err); - } - } - ); + }); } } -- 2.47.3 From 5a13cf92140b402bcecbbc7f787e70b6511f3213 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Fri, 4 Oct 2024 17:05:10 +0200 Subject: [PATCH 17/28] administration done (minimalistic) --- src/main.rs | 162 ++++++++++++++++++++++++++++++----------- static/html/admin.html | 6 +- static/js/admin.js | 58 ++++++++++++++- 3 files changed, 178 insertions(+), 48 deletions(-) diff --git a/src/main.rs b/src/main.rs index afcf5fb..74c6746 100644 --- a/src/main.rs +++ b/src/main.rs @@ -293,26 +293,37 @@ async fn get_admin( db: Arc>, ) -> Result, Error> { let authorised = is_authorised(req, db.clone(), 3).await; - if authorised { - return not_found().await; + if !authorised { + return get_page(&req, "/unauthorised", db).await; } - if path == "/admin" { - return get_page(req, path, db).await; + match path { + "/admin" => { + get_page(req, path, db).await}, + "/admin/users" => { + let pool = db.clone().lock().unwrap().clone(); + let users = sqlx::query!(r#"SELECT id, username, permissions FROM users"#) + .fetch_all(&pool) + .await + .unwrap(); + let users: Vec<(i64, String, i64)> = users + .iter() + .map(|x| (x.id, x.username.clone(), x.permissions)) + .collect(); + let stringed = serde_json::to_string(&users).unwrap_or("".to_string()); + Ok(Response::builder().body(Body::new(stringed)).unwrap()) + }, + "/admin/players" => { + let pool = db.clone().lock().unwrap().clone(); + let players = sqlx::query!(r#"SELECT id, name FROM players"#) + .fetch_all(&pool) + .await + .unwrap(); + let players: Vec = players.iter().map(|x| Player{id: x.id, name: x.name.clone()}).collect(); + let stringed = serde_json::to_string(&players).unwrap_or("".to_string()); + Ok(Response::builder().body(Body::new(stringed)).unwrap()) + } + _ => not_found().await, } - if path == "/admin/users" { - let pool = db.clone().lock().unwrap().clone(); - let users = sqlx::query!(r#"SELECT username, permissions FROM users"#) - .fetch_all(&pool) - .await - .unwrap(); - let users: Vec<(String, i64)> = users - .iter() - .map(|x| (x.username.clone(), x.permissions)) - .collect(); - let stringed = serde_json::to_string(&users).unwrap_or("".to_string()); - return Ok(Response::builder().body(Body::new(stringed)).unwrap()); - } - not_found().await } async fn post(req: Request, db: Arc>) -> Result, Error> { @@ -369,19 +380,92 @@ async fn post_admin( db: Arc>, ) -> Result, Error> { let authorised = is_authorised(&req, db.clone(), 3).await; - if authorised { + if !authorised { return get_page(&req, "/unauthorised", db).await; } let path = req.uri().path(); match path { - "/admin/post/user" => { - req_json::(req).await; + "/admin/edit/user" => { + match req_json::(req).await { + Some(Value::Object(user)) => { + let username = user.get("username"); + let permissions = user.get("permissions"); + let id = user.get("id"); + if username.is_none() || permissions.is_none() || id.is_none() { + return bad_request().await; + } + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let username = username.unwrap().as_str().unwrap(); + let permissions = permissions.unwrap(); + let id = id.unwrap(); + let _ = sqlx::query!(r#"UPDATE users SET username = ?1, permissions = ?2 WHERE id = ?3"#, username, permissions, id).execute(&mut *conn).await; + ok().await + }, + _ => {bad_request().await} + } + }, + "/admin/delete/user" => { + match req_json::(req).await { + Some(Value::Object(user)) => { + let id = user.get("id"); + if id.is_none() { + return bad_request().await; + } + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let id = id.unwrap().as_i64().unwrap(); + let _ = sqlx::query!(r#"DELETE FROM users WHERE id = ?1"#, id).execute(&mut *conn).await; + ok().await + } + _ => {bad_request().await} + } + }, + "/admin/edit/player" => { + match req_json::(req).await { + Some(player) => { + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let _ = sqlx::query!(r#"UPDATE players SET name = ?1 WHERE id = ?2"#, player.name, player.id).execute(&mut *conn).await; + ok().await + }, + _ => bad_request().await + } } - "/admin/post/vote" => {} - "/admin/post/player" => {} - _ => {} + "/admin/new/player" => { + match req_json::(req).await { + Some(Value::Object(player)) => { + let name = player.get("name"); + if name.is_none() { + return bad_request().await; + } + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let name = name.unwrap().as_str().unwrap(); + let _ = sqlx::query!(r#"INSERT INTO players (name) VALUES (?1)"#, name).execute(&mut *conn).await; + ok().await + }, + _ => {bad_request().await} + } + }, + "/admin/delete/player" => { + match req_json::(req).await { + Some(Value::Object(player)) => { + let id = player.get("id"); + if id.is_none() { + return bad_request().await; + } + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let id = id.unwrap().as_i64().unwrap(); + let _ = sqlx::query!(r#"DELETE FROM players WHERE id = ?1"#, id).execute(&mut *conn).await; + ok().await + } + _ => {bad_request().await} + } + } + _ => {bad_request().await} } - not_found().await } async fn login( @@ -391,17 +475,11 @@ async fn login( let body = req.into_body().collect().await; let data: Result = from_reader(body?.aggregate().reader()); if data.is_err() { - return Ok(Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::Empty) - .unwrap()); + return bad_request().await; } let data = data.unwrap(); if !check_username(&data.username) { - return Ok(Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::Empty) - .unwrap()); + return bad_request().await; } let pool = db.clone().lock().unwrap().clone(); let result = sqlx::query!(r#"SELECT * FROM users WHERE username=?1"#, data.username) @@ -415,7 +493,6 @@ async fn login( Ok(()) => { let date: DateTime = DateTime::from(SystemTime::now()); let date = date.checked_add_days(Days::new(7)).unwrap(); - // With server side rendering, redirect here to "/" Ok(Response::builder() .header( SET_COOKIE, @@ -435,16 +512,10 @@ async fn login( .body(Body::Empty) .unwrap()) } - Err(_) => Ok(Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::Empty) - .unwrap()), + Err(_) => bad_request().await, } } - Ok(None) => Ok(Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::Empty) - .unwrap()), + Ok(None) => bad_request().await, Err(_) => Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::Empty) @@ -599,6 +670,13 @@ async fn not_found() -> Result, Error> { .unwrap()) } +async fn bad_request() -> Result, Error> { + Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Body::Empty).unwrap()) +} + +async fn ok() -> Result, Error> { + Ok(Response::builder().body(Body::Empty).unwrap()) +} async fn req_json(req: Request) -> Option where T: DeserializeOwned, diff --git a/static/html/admin.html b/static/html/admin.html index f5db6f6..6cca1a8 100644 --- a/static/html/admin.html +++ b/static/html/admin.html @@ -12,11 +12,13 @@

Users

+

id, username, permission

-

Votes

-
+

Players

+

id, name

+
\ No newline at end of file diff --git a/static/js/admin.js b/static/js/admin.js index 02117b8..bf8dbf1 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -4,19 +4,69 @@ async function run() { let usersDiv = document.getElementById("users"); for (let i = 0; i < users.length; i++) { let item = document.createElement("div"); + let id = document.createElement("p"); let username = document.createElement("input"); let permissions = document.createElement("input"); let edit = document.createElement("button"); + let del = document.createElement("button"); edit.textContent = "Edit"; - username.value = users[i][0]; - permissions.value = users[i][1]; + del.textContent = "Delete"; + id.textContent = users[i][0]; + username.value = users[i][1]; + permissions.value = users[i][2]; item.style.display = "flex"; - item.append(username, permissions, edit); + item.append(id, username, permissions, edit, del); usersDiv.appendChild(item); + edit.addEventListener("click", async () => { + await fetch("/admin/edit/user", {method: "POST", body: JSON.stringify({"id": users[i][0], "username": username.value, "permissions": parseInt(permissions.value)})}); + window.location.reload(); + }) + del.addEventListener("click", async () => { + await fetch("/admin/delete/user", {method:"POST", body: JSON.stringify({"id": users[i][0]})}); + window.location.reload(); + }) } -// let votes = await fetch("/admin/votes").then(r => r.json()); + let players = await fetch("/admin/players").then(r => r.json()); + let playersDiv = document.getElementById("players"); + for (let i=0; i { + await fetch("/admin/edit/player", {method: "POST", body: JSON.stringify({"id": players[i]["id"], "name": name.value})}); + window.location.reload(); + }) + del.addEventListener("click", async () => { + await fetch("/admin/delete/player", {method:"POST", body: JSON.stringify({"id": players[i]["id"]})}); + window.location.reload(); + }) + } + let newPlayer = document.createElement("button"); + newPlayer.textContent = "Add Player"; + newPlayer.addEventListener("click", () => { + let item = document.createElement("div"); + let name = document.createElement("input"); + let save = document.createElement("button"); + save.textContent = "Save"; + item.append(name, save); + playersDiv.appendChild(item); + save.addEventListener("click", async () => { + await fetch("/admin/new/player", {method:"POST", body: JSON.stringify({"name": name.value})}) + window.location.reload(); + }) + }) + playersDiv.parentNode.append(newPlayer); } let _ = run(); \ No newline at end of file -- 2.47.3 From 01c8248313ca3a003ca42a97daf8ec8d5dfc7ec9 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Fri, 4 Oct 2024 17:05:39 +0200 Subject: [PATCH 18/28] bug fix: ids can be wrong if players added/deleted --- static/css/results.css | 5 ++--- static/js/results.js | 28 ++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/static/css/results.css b/static/css/results.css index 0c1f07d..7ea1742 100644 --- a/static/css/results.css +++ b/static/css/results.css @@ -26,9 +26,8 @@ body { } .app { - margin: auto; - margin-bottom: 60px; - height: calc(100dvh + 60px); + margin: auto auto 60px; + height: calc(100% + 60px); width: 100%; max-width: 500px; } diff --git a/static/js/results.js b/static/js/results.js index 3d2fbb4..ed4c5c5 100644 --- a/static/js/results.js +++ b/static/js/results.js @@ -23,7 +23,12 @@ function show_plus(id, votes, players) { const app = document.getElementById("app"); app.innerHTML = ""; let vote = votes[id]; - let player = players[vote["plus_player_id"] - 1]["name"]; + let player = ""; + for (let i = 0; i < players.length; i++) { + if (players[i].id === vote["plus_player_id"]) { + player = players[i]["name"]; + } + } let nickname = vote["plus_nickname"]; let reason = vote["plus_reason"]; let minus = document.createElement("button"); @@ -58,7 +63,12 @@ function show_minus(id, votes, players) { let vote = votes[id]; let nickname = vote["minus_nickname"]; let reason = vote["minus_reason"]; - let player = players[vote["minus_player_id"] - 1]["name"]; + let player = ""; + for (let i = 0; i < players.length; i++) { + if (players[i].id === vote["minus_player_id"]) { + player = players[i]["name"]; + } + } let next = document.createElement("button"); if (id === votes.length - 1) { next.textContent = "Résultats"; @@ -112,7 +122,12 @@ async function show_results(players) { let counter = 0; for (let i = 0; i < plus.length; i++) { let p = plus[i]; - let player = players[p[0] - 1]["name"]; + let player = ""; + for (let i = 0; i < players.length; i++) { + if (players[i].id === p[0]) { + player = players[i]["name"]; + } + } let score = p[1]; if (prev_score == null || score < prev_score) { counter += 1; @@ -134,7 +149,12 @@ async function show_results(players) { counter = 0; for (let i = 0; i < minus.length; i++) { let p = minus[i]; - let player = players[p[0] - 1]["name"]; + let player = ""; + for (let i = 0; i < players.length; i++) { + if (players[i].id === p[0]) { + player = players[i]["name"]; + } + } let score = p[1]; if (prev_score == null || score < prev_score) { counter += 1; -- 2.47.3 From cd1632b53396b94393523b213a2a5250102e4b72 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Fri, 4 Oct 2024 17:15:59 +0200 Subject: [PATCH 19/28] added check for token generation (look if exists) --- src/main.rs | 201 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 116 insertions(+), 85 deletions(-) diff --git a/src/main.rs b/src/main.rs index 74c6746..fea44d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use chrono::{DateTime, Days, Utc}; use daemonize::Daemonize; use http_body_util::{BodyExt, Full}; use hyper::{ - body::{Body as HyperBody, Incoming, Frame}, + body::{Body as HyperBody, Frame, Incoming}, header::{COOKIE, SET_COOKIE}, server::conn::http1, service::service_fn, @@ -134,7 +134,12 @@ async fn get_page( let map: Value = from_reader(file).expect("Could not parse routes, please verify syntax."); match map.get(path) { Some(Value::Object(s)) => { - let authorised = is_authorised(req, db, s.get("permission").unwrap().as_u64().unwrap() as u8).await; + let authorised = is_authorised( + req, + db, + s.get("permission").unwrap().as_u64().unwrap() as u8, + ) + .await; if authorised { get_file(s.get("file").unwrap().as_str().unwrap()).await } else { @@ -297,8 +302,7 @@ async fn get_admin( return get_page(&req, "/unauthorised", db).await; } match path { - "/admin" => { - get_page(req, path, db).await}, + "/admin" => get_page(req, path, db).await, "/admin/users" => { let pool = db.clone().lock().unwrap().clone(); let users = sqlx::query!(r#"SELECT id, username, permissions FROM users"#) @@ -311,14 +315,20 @@ async fn get_admin( .collect(); let stringed = serde_json::to_string(&users).unwrap_or("".to_string()); Ok(Response::builder().body(Body::new(stringed)).unwrap()) - }, + } "/admin/players" => { let pool = db.clone().lock().unwrap().clone(); let players = sqlx::query!(r#"SELECT id, name FROM players"#) .fetch_all(&pool) .await .unwrap(); - let players: Vec = players.iter().map(|x| Player{id: x.id, name: x.name.clone()}).collect(); + let players: Vec = players + .iter() + .map(|x| Player { + id: x.id, + name: x.name.clone(), + }) + .collect(); let stringed = serde_json::to_string(&players).unwrap_or("".to_string()); Ok(Response::builder().body(Body::new(stringed)).unwrap()) } @@ -385,86 +395,95 @@ async fn post_admin( } let path = req.uri().path(); match path { - "/admin/edit/user" => { - match req_json::(req).await { - Some(Value::Object(user)) => { - let username = user.get("username"); - let permissions = user.get("permissions"); - let id = user.get("id"); - if username.is_none() || permissions.is_none() || id.is_none() { - return bad_request().await; - } - let pool = db.clone().lock().unwrap().clone(); - let mut conn = pool.acquire().await.unwrap(); - let username = username.unwrap().as_str().unwrap(); - let permissions = permissions.unwrap(); - let id = id.unwrap(); - let _ = sqlx::query!(r#"UPDATE users SET username = ?1, permissions = ?2 WHERE id = ?3"#, username, permissions, id).execute(&mut *conn).await; - ok().await - }, - _ => {bad_request().await} - } - }, - "/admin/delete/user" => { - match req_json::(req).await { - Some(Value::Object(user)) => { - let id = user.get("id"); - if id.is_none() { - return bad_request().await; - } - let pool = db.clone().lock().unwrap().clone(); - let mut conn = pool.acquire().await.unwrap(); - let id = id.unwrap().as_i64().unwrap(); - let _ = sqlx::query!(r#"DELETE FROM users WHERE id = ?1"#, id).execute(&mut *conn).await; - ok().await + "/admin/edit/user" => match req_json::(req).await { + Some(Value::Object(user)) => { + let username = user.get("username"); + let permissions = user.get("permissions"); + let id = user.get("id"); + if username.is_none() || permissions.is_none() || id.is_none() { + return bad_request().await; } - _ => {bad_request().await} + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let username = username.unwrap().as_str().unwrap(); + let permissions = permissions.unwrap(); + let id = id.unwrap(); + let _ = sqlx::query!( + r#"UPDATE users SET username = ?1, permissions = ?2 WHERE id = ?3"#, + username, + permissions, + id + ) + .execute(&mut *conn) + .await; + ok().await } + _ => bad_request().await, }, - "/admin/edit/player" => { - match req_json::(req).await { - Some(player) => { - let pool = db.clone().lock().unwrap().clone(); - let mut conn = pool.acquire().await.unwrap(); - let _ = sqlx::query!(r#"UPDATE players SET name = ?1 WHERE id = ?2"#, player.name, player.id).execute(&mut *conn).await; - ok().await - }, - _ => bad_request().await - } - } - "/admin/new/player" => { - match req_json::(req).await { - Some(Value::Object(player)) => { - let name = player.get("name"); - if name.is_none() { - return bad_request().await; - } - let pool = db.clone().lock().unwrap().clone(); - let mut conn = pool.acquire().await.unwrap(); - let name = name.unwrap().as_str().unwrap(); - let _ = sqlx::query!(r#"INSERT INTO players (name) VALUES (?1)"#, name).execute(&mut *conn).await; - ok().await - }, - _ => {bad_request().await} - } - }, - "/admin/delete/player" => { - match req_json::(req).await { - Some(Value::Object(player)) => { - let id = player.get("id"); - if id.is_none() { - return bad_request().await; - } - let pool = db.clone().lock().unwrap().clone(); - let mut conn = pool.acquire().await.unwrap(); - let id = id.unwrap().as_i64().unwrap(); - let _ = sqlx::query!(r#"DELETE FROM players WHERE id = ?1"#, id).execute(&mut *conn).await; - ok().await + "/admin/delete/user" => match req_json::(req).await { + Some(Value::Object(user)) => { + let id = user.get("id"); + if id.is_none() { + return bad_request().await; } - _ => {bad_request().await} + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let id = id.unwrap().as_i64().unwrap(); + let _ = sqlx::query!(r#"DELETE FROM users WHERE id = ?1"#, id) + .execute(&mut *conn) + .await; + ok().await } - } - _ => {bad_request().await} + _ => bad_request().await, + }, + "/admin/edit/player" => match req_json::(req).await { + Some(player) => { + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let _ = sqlx::query!( + r#"UPDATE players SET name = ?1 WHERE id = ?2"#, + player.name, + player.id + ) + .execute(&mut *conn) + .await; + ok().await + } + _ => bad_request().await, + }, + "/admin/new/player" => match req_json::(req).await { + Some(Value::Object(player)) => { + let name = player.get("name"); + if name.is_none() { + return bad_request().await; + } + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let name = name.unwrap().as_str().unwrap(); + let _ = sqlx::query!(r#"INSERT INTO players (name) VALUES (?1)"#, name) + .execute(&mut *conn) + .await; + ok().await + } + _ => bad_request().await, + }, + "/admin/delete/player" => match req_json::(req).await { + Some(Value::Object(player)) => { + let id = player.get("id"); + if id.is_none() { + return bad_request().await; + } + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let id = id.unwrap().as_i64().unwrap(); + let _ = sqlx::query!(r#"DELETE FROM players WHERE id = ?1"#, id) + .execute(&mut *conn) + .await; + ok().await + } + _ => bad_request().await, + }, + _ => bad_request().await, } } @@ -561,7 +580,16 @@ async fn register( .hash_password(login.password.as_bytes(), &SaltString::generate(&mut OsRng)) .unwrap() .to_string(); - let token = Alphanumeric.sample_string(&mut OsRng, 256); + let mut token = Alphanumeric.sample_string(&mut OsRng, 256); + while match sqlx::query!(r#"SELECT id FROM users WHERE token=?1"#, token) + .fetch_optional(&mut *conn) + .await + { + Ok(Some(user)) => true, + _ => false, + } { + token = Alphanumeric.sample_string(&mut OsRng, 256); + } let result = sqlx::query!(r#"INSERT INTO users ( username, saltyhash, permissions, token) VALUES ( ?1, ?2, ?3, ?4 )"#, login.username, hash, 0, token).execute(&mut *conn).await; match result { Ok(_) => Ok(Response::builder().body(Body::Empty).unwrap()), @@ -616,10 +644,10 @@ async fn is_authorised(req: &Request, db: Arc>, leve Ok(Some(user)) => { let perm = user.permissions as u8; perm >= level - }, + } _ => match level { 0 => true, - _ => false + _ => false, }, } } @@ -671,7 +699,10 @@ async fn not_found() -> Result, Error> { } async fn bad_request() -> Result, Error> { - Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Body::Empty).unwrap()) + Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::Empty) + .unwrap()) } async fn ok() -> Result, Error> { -- 2.47.3 From fb282218757d606fbd90353eed37a140f4fe2972 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Fri, 4 Oct 2024 17:17:46 +0200 Subject: [PATCH 20/28] added check for token generation (look if exists) (better) --- src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index fea44d9..8c1f1e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -581,13 +581,10 @@ async fn register( .unwrap() .to_string(); let mut token = Alphanumeric.sample_string(&mut OsRng, 256); - while match sqlx::query!(r#"SELECT id FROM users WHERE token=?1"#, token) + while let Ok(Some(_)) = sqlx::query!(r#"SELECT id FROM users WHERE token=?1"#, token) .fetch_optional(&mut *conn) .await { - Ok(Some(user)) => true, - _ => false, - } { token = Alphanumeric.sample_string(&mut OsRng, 256); } let result = sqlx::query!(r#"INSERT INTO users ( username, saltyhash, permissions, token) VALUES ( ?1, ?2, ?3, ?4 )"#, login.username, hash, 0, token).execute(&mut *conn).await; -- 2.47.3 From 28b2411ceb62c4cc3dd27bda6a806face9e1efc8 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Fri, 4 Oct 2024 17:28:20 +0200 Subject: [PATCH 21/28] user can add player --- src/main.rs | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8c1f1e6..740979a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -346,6 +346,7 @@ async fn post(req: Request, db: Arc>) -> Result login(req, db).await, "/register" => register(req, db).await, "/logout" => logout().await, + "/player" => post_player(req, db).await, _ => not_found().await, } } @@ -354,13 +355,9 @@ async fn post_vote( req: Request, db: Arc>, ) -> Result, Error> { - let body = req.into_body().collect().await?; - let data: Result = from_reader(body.aggregate().reader()); - if data.is_err() { - return Ok(Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::Empty) - .unwrap()); + let data = req_json::(req).await; + if data.is_none() { + return bad_request().await; } let vote = data.unwrap(); let timestamp: DateTime = DateTime::from(SystemTime::now()); @@ -382,7 +379,26 @@ async fn post_vote( .body(Body::Empty) .unwrap()); } - Ok(Response::builder().body(Body::Empty).unwrap()) + ok().await +} + +async fn post_player(req: Request, db: Arc>) -> Result, Error> { + let data = req_json::(req).await; + if data.is_none() { + return bad_request().await; + } + let data = data.unwrap(); + let name = data.get("name").unwrap().as_str().unwrap(); + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let r = sqlx::query!(r#"INSERT INTO players (name) VALUES (?1)"#, data).execute(&mut *conn).await; + if r.is_err() { + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::Empty) + .unwrap()); + } + ok().await } async fn post_admin( -- 2.47.3 From 9c8e8beaa6ca583d9c9744f08a9c50d8e7fda5cd Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Fri, 4 Oct 2024 21:00:43 +0200 Subject: [PATCH 22/28] user can add player while voting --- static/html/index.html | 2 ++ static/js/index.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/static/html/index.html b/static/html/index.html index d6482c2..6474231 100644 --- a/static/html/index.html +++ b/static/html/index.html @@ -16,6 +16,8 @@

Vote +

+ + diff --git a/static/js/index.js b/static/js/index.js index da35d26..daf8559 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -24,10 +24,25 @@ async function main() { option.textContent = x["name"]; select.append(option); }) + let other = document.createElement("option"); + other.value = "other"; + other.textContent = "Autre"; + select.append(other); select.value = null; nickname.value = ""; reason.value = ""; select.addEventListener("change", () => { + if (select.value === "other") { + let other = document.getElementById("other"); + other.hidden = false; + let otherLabel = document.getElementById("other_label"); + otherLabel.hidden = false; + } else { + let other = document.getElementById("other"); + other.hidden = true; + let otherLabel = document.getElementById("other_label"); + otherLabel.hidden = true; + } if (current_page) { vote.minus_player_id = parseInt(select.value); } else { @@ -59,6 +74,19 @@ async function main() { true, "warning"); return; } + if (select.value === "other") { + let otherInput = document.getElementById("other"); + let otherInputLabel = document.getElementById("other_label"); + let player = await fetch("/player", {method: "post", body: JSON.stringify({"name": otherInput.value})}).then(r => r.json()); + let option = document.createElement("option"); + option.value = player["id"]; + option.textContent = player["name"]; + select.insertBefore(option, other); + vote.minus_player_id = parseInt(player["id"]); + otherInput.value = ""; + otherInput.hidden = true; + otherInputLabel.hidden = true; + } if (await fetch("/vote", { method: "post", body: JSON.stringify(vote) }) @@ -76,6 +104,19 @@ async function main() { } rightButton.textContent = "À voté!"; title.textContent = "Vote -"; + if (select.value === "other") { + let otherInput = document.getElementById("other"); + let otherInputLabel = document.getElementById("other_label"); + let player = await fetch("/player", {method: "post", body: JSON.stringify({"name": otherInput.value})}).then(r => r.json()); + let option = document.createElement("option"); + option.value = player["id"]; + option.textContent = player["name"]; + select.insertBefore(option, other); + vote.plus_player_id = parseInt(player["id"]); + otherInput.value = ""; + otherInput.hidden = true; + otherInputLabel.hidden = true; + } current_page = 1; leftButton.hidden = false; select.value = vote.minus_player_id; -- 2.47.3 From 164184e5e9e62e1457d7c0e8befe2ecc029c6e8d Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Fri, 4 Oct 2024 21:00:53 +0200 Subject: [PATCH 23/28] user can add player while voting --- src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 740979a..a5b71f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -391,14 +391,19 @@ async fn post_player(req: Request, db: Arc>) -> Resu let name = data.get("name").unwrap().as_str().unwrap(); let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); - let r = sqlx::query!(r#"INSERT INTO players (name) VALUES (?1)"#, data).execute(&mut *conn).await; + if let Ok(Some(player)) = sqlx::query!(r#"SELECT * FROM players WHERE name = ?1"#, name).fetch_optional(&pool).await { + let player = Player{id: player.id, name: player.name}; + return Ok(Response::builder().body(Body::new(serde_json::to_string(&player).unwrap())).unwrap()); + } + let r = sqlx::query!(r#"INSERT INTO players (name) VALUES (?1) RETURNING id"#, name).fetch_one(&mut *conn).await; if r.is_err() { return Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::Empty) .unwrap()); } - ok().await + let player = Player{id: r.unwrap().id, name: name.to_string()}; + Ok(Response::builder().body(Body::new(serde_json::to_string(&player).unwrap())).unwrap()) } async fn post_admin( -- 2.47.3 From 92c067c9ed82c5a8cf2a19045cade41e32465662 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Fri, 4 Oct 2024 22:49:00 +0200 Subject: [PATCH 24/28] small fixes (if player not exsits show ?) --- static/js/index.js | 8 ++++++++ static/js/results.js | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index daf8559..4ce1531 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -76,6 +76,10 @@ async function main() { } if (select.value === "other") { let otherInput = document.getElementById("other"); + if (otherInput.value === "") { + showMessage("Tu as sélectionné autre mais tu n'as pas donné de nom", "N'oublie pas de soit sélectioner une personne ou de mettre un nom dans ''Autre:''.", true, "warning"); + return; + } let otherInputLabel = document.getElementById("other_label"); let player = await fetch("/player", {method: "post", body: JSON.stringify({"name": otherInput.value})}).then(r => r.json()); let option = document.createElement("option"); @@ -106,6 +110,10 @@ async function main() { title.textContent = "Vote -"; if (select.value === "other") { let otherInput = document.getElementById("other"); + if (otherInput.value === "") { + showMessage("Tu as sélectionné autre mais tu n'as pas donné de nom", "N'oublie pas de soit sélectioner une personne ou de mettre un nom dans ''Autre:''.", true, "warning"); + return; + } let otherInputLabel = document.getElementById("other_label"); let player = await fetch("/player", {method: "post", body: JSON.stringify({"name": otherInput.value})}).then(r => r.json()); let option = document.createElement("option"); diff --git a/static/js/results.js b/static/js/results.js index ed4c5c5..838c5b9 100644 --- a/static/js/results.js +++ b/static/js/results.js @@ -23,7 +23,7 @@ function show_plus(id, votes, players) { const app = document.getElementById("app"); app.innerHTML = ""; let vote = votes[id]; - let player = ""; + let player = "?"; for (let i = 0; i < players.length; i++) { if (players[i].id === vote["plus_player_id"]) { player = players[i]["name"]; @@ -63,7 +63,7 @@ function show_minus(id, votes, players) { let vote = votes[id]; let nickname = vote["minus_nickname"]; let reason = vote["minus_reason"]; - let player = ""; + let player = "?"; for (let i = 0; i < players.length; i++) { if (players[i].id === vote["minus_player_id"]) { player = players[i]["name"]; @@ -122,7 +122,7 @@ async function show_results(players) { let counter = 0; for (let i = 0; i < plus.length; i++) { let p = plus[i]; - let player = ""; + let player = "?"; for (let i = 0; i < players.length; i++) { if (players[i].id === p[0]) { player = players[i]["name"]; @@ -149,7 +149,7 @@ async function show_results(players) { counter = 0; for (let i = 0; i < minus.length; i++) { let p = minus[i]; - let player = ""; + let player = "?"; for (let i = 0; i < players.length; i++) { if (players[i].id === p[0]) { player = players[i]["name"]; -- 2.47.3 From c07a304562513f551ac44eee3d13813d0137f4e8 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Fri, 4 Oct 2024 22:49:51 +0200 Subject: [PATCH 25/28] Votes div remains to have date displayed per vote --- src/main.rs | 100 ++++++++++++++++++++++++++++++++++++++--- static/html/admin.html | 6 +++ static/js/admin.js | 48 ++++++++++++++++++++ 3 files changed, 147 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index a5b71f1..78b506e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,6 +68,7 @@ struct Player { #[derive(Serialize, Deserialize)] struct Vote { + id: i64, plus_player_id: i64, plus_nickname: String, plus_reason: String, @@ -282,6 +283,7 @@ async fn get_votes(req: &Request, db: Arc>) -> Vec { + let pool = db.clone().lock().unwrap().clone(); + let votes = sqlx::query!(r#"SELECT * FROM votes"#) + .fetch_all(&pool) + .await + .unwrap(); + let votes: Vec = votes + .iter() + .map(|x| Vote { + id: x.id, + plus_player_id: x.plus_player_id, + plus_nickname: x.plus_nickname.clone(), + plus_reason: x.plus_reason.clone(), + minus_player_id: x.minus_player_id, + minus_nickname: x.minus_nickname.clone(), + minus_reason: x.minus_reason.clone(), + }) + .collect(); + let stringed = serde_json::to_string(&votes).unwrap_or("".to_string()); + Ok(Response::builder().body(Body::new(stringed)).unwrap()) + } _ => not_found().await, } } @@ -382,7 +405,10 @@ async fn post_vote( ok().await } -async fn post_player(req: Request, db: Arc>) -> Result, Error> { +async fn post_player( + req: Request, + db: Arc>, +) -> Result, Error> { let data = req_json::(req).await; if data.is_none() { return bad_request().await; @@ -391,19 +417,37 @@ async fn post_player(req: Request, db: Arc>) -> Resu let name = data.get("name").unwrap().as_str().unwrap(); let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); - if let Ok(Some(player)) = sqlx::query!(r#"SELECT * FROM players WHERE name = ?1"#, name).fetch_optional(&pool).await { - let player = Player{id: player.id, name: player.name}; - return Ok(Response::builder().body(Body::new(serde_json::to_string(&player).unwrap())).unwrap()); + if let Ok(Some(player)) = sqlx::query!(r#"SELECT * FROM players WHERE name = ?1"#, name) + .fetch_optional(&pool) + .await + { + let player = Player { + id: player.id, + name: player.name, + }; + return Ok(Response::builder() + .body(Body::new(serde_json::to_string(&player).unwrap())) + .unwrap()); } - let r = sqlx::query!(r#"INSERT INTO players (name) VALUES (?1) RETURNING id"#, name).fetch_one(&mut *conn).await; + let r = sqlx::query!( + r#"INSERT INTO players (name) VALUES (?1) RETURNING id"#, + name + ) + .fetch_one(&mut *conn) + .await; if r.is_err() { return Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::Empty) .unwrap()); } - let player = Player{id: r.unwrap().id, name: name.to_string()}; - Ok(Response::builder().body(Body::new(serde_json::to_string(&player).unwrap())).unwrap()) + let player = Player { + id: r.unwrap().id, + name: name.to_string(), + }; + Ok(Response::builder() + .body(Body::new(serde_json::to_string(&player).unwrap())) + .unwrap()) } async fn post_admin( @@ -504,6 +548,48 @@ async fn post_admin( } _ => bad_request().await, }, + "/admin/edit/vote" => match req_json::(req).await { + Some(vote) => { + let pool = db.clone().lock().unwrap().clone(); + let _ = sqlx::query!( + r#"UPDATE votes + SET plus_player_id = ?1, + plus_nickname = ?2, + plus_reason = ?3, + minus_player_id = ?4, + minus_nickname = ?5, + minus_reason = ?6 + WHERE id = ?7"#, + vote.plus_player_id, + vote.plus_nickname, + vote.plus_reason, + vote.minus_player_id, + vote.minus_nickname, + vote.minus_reason, + vote.id + ) + .execute(&pool) + .await; + ok().await + } + _ => bad_request().await, + }, + "/admin/delete/vote" => match req_json::(req).await { + Some(Value::Object(vote)) => { + let id = vote.get("id"); + if id.is_none() { + return bad_request().await; + } + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let id = id.unwrap().as_i64().unwrap(); + let _ = sqlx::query!(r#"DELETE FROM votes WHERE id = ?1"#, id) + .execute(&mut *conn) + .await; + ok().await + } + _ => bad_request().await, + } _ => bad_request().await, } } diff --git a/static/html/admin.html b/static/html/admin.html index 6cca1a8..0fc3176 100644 --- a/static/html/admin.html +++ b/static/html/admin.html @@ -20,5 +20,11 @@

id, name

+
+

Votes

+

+

id, plus_id, plus_nickname, plus_reason, minus_id, minus_nickname, minus_reason

+
+
\ No newline at end of file diff --git a/static/js/admin.js b/static/js/admin.js index bf8dbf1..6f72061 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -67,6 +67,54 @@ async function run() { }) }) playersDiv.parentNode.append(newPlayer); + + let votes = await fetch("/admin/votes").then(r => r.json()); + let today = document.getElementById("votes_number"); + let count = await fetch("/data/votes").then(r => r.json()); + today.textContent = `Aujourd'hui il y a ${count.length} votes`; + let votesDiv = document.getElementById("votes"); + + for (let i=0; i { + await fetch("/admin/edit/vote", {method: "POST", body: JSON.stringify({"id": votes[i]["id"], + "plus_player_id": parseInt(plus_id.value), + "plus_nickname": plus_nickname.value, + "plus_reason": plus_reason.value, + "minus_player_id": parseInt(minus_id.value), + "minus_nickname": minus_nickname.value, + "minus_reason": minus_reason.value})}); + window.location.reload(); + }) + del.addEventListener("click", async () => { + await fetch("/admin/delete/vote", {method:"POST", body: JSON.stringify({"id": votes[i]["id"]})}); + window.location.reload(); + }) + } } let _ = run(); \ No newline at end of file -- 2.47.3 From 98dbcd11f1ecca44076eceaef247ad6117d0f9ea Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 5 Oct 2024 15:08:41 +0200 Subject: [PATCH 26/28] Added submit date to votes --- static/html/admin.html | 2 +- static/js/admin.js | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/static/html/admin.html b/static/html/admin.html index 0fc3176..7ca45c8 100644 --- a/static/html/admin.html +++ b/static/html/admin.html @@ -23,7 +23,7 @@

Votes

-

id, plus_id, plus_nickname, plus_reason, minus_id, minus_nickname, minus_reason

+

id, submit_date, plus_id, plus_nickname, plus_reason, minus_id, minus_nickname, minus_reason

diff --git a/static/js/admin.js b/static/js/admin.js index 6f72061..cb2d599 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -80,6 +80,8 @@ async function run() { item.style.display = "flex"; let id = document.createElement("p"); id.textContent = vote["id"]; + let submit_date = document.createElement("input"); + submit_date.value = vote["submit_date"]; let plus_id = document.createElement("input"); plus_id.type = "number"; plus_id.value = vote["plus_player_id"]; @@ -98,16 +100,17 @@ async function run() { edit.textContent = "Edit"; let del = document.createElement("button"); del.textContent = "Delete"; - item.append(id,plus_id,plus_nickname,plus_reason,minus_id,minus_nickname,minus_reason, edit, del); + item.append(id,submit_date,plus_id,plus_nickname,plus_reason,minus_id,minus_nickname,minus_reason, edit, del); votesDiv.append(item); edit.addEventListener("click", async () => { await fetch("/admin/edit/vote", {method: "POST", body: JSON.stringify({"id": votes[i]["id"], - "plus_player_id": parseInt(plus_id.value), - "plus_nickname": plus_nickname.value, - "plus_reason": plus_reason.value, - "minus_player_id": parseInt(minus_id.value), - "minus_nickname": minus_nickname.value, - "minus_reason": minus_reason.value})}); + "submit_date": submit_date.value, + "plus_player_id": parseInt(plus_id.value), + "plus_nickname": plus_nickname.value, + "plus_reason": plus_reason.value, + "minus_player_id": parseInt(minus_id.value), + "minus_nickname": minus_nickname.value, + "minus_reason": minus_reason.value})}); window.location.reload(); }) del.addEventListener("click", async () => { -- 2.47.3 From 8a7c321044125992147e1cc843f39a31e6ef0dd8 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 5 Oct 2024 15:09:51 +0200 Subject: [PATCH 27/28] Fixed bug related to new Vote struct, id and submit date can be None (useful for posting new votes, ie without id and submit date) --- src/main.rs | 118 +++++++++++++++++------------------------ static/html/index.html | 2 +- static/js/index.js | 12 +---- 3 files changed, 53 insertions(+), 79 deletions(-) diff --git a/src/main.rs b/src/main.rs index 78b506e..5d7711a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use argon2::{ Argon2, }; use bytes::{Buf, Bytes}; -use chrono::{DateTime, Days, Utc}; +use chrono::{DateTime, Days, NaiveTime, Timelike, Utc}; #[cfg(target_os = "linux")] use daemonize::Daemonize; use http_body_util::{BodyExt, Full}; @@ -66,9 +66,10 @@ struct Player { name: String, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] struct Vote { - id: i64, + id: Option, + submit_date: Option, plus_player_id: i64, plus_nickname: String, plus_reason: String, @@ -100,9 +101,9 @@ async fn service( req: Request, db: Arc>, ) -> Result, Error> { - match req.method() { - &Method::GET => get(req, db).await, - &Method::POST => post(req, db).await, + match *req.method() { + Method::GET => get(req, db).await, + Method::POST => post(req, db).await, _ => Ok(Response::builder() .status(StatusCode::IM_A_TEAPOT) .body(Body::Empty) @@ -224,17 +225,13 @@ async fn get_data( let mut plus_results: HashMap = HashMap::new(); let mut minus_results: HashMap = HashMap::new(); - let _ = ids.iter().for_each(|x| { + ids.iter().for_each(|x| { let plus_id = x.0; - if !plus_results.contains_key(&plus_id) { - plus_results.insert(plus_id, 0); - } + plus_results.entry(plus_id).or_insert(0); *plus_results.get_mut(&plus_id).unwrap() += 1; let minus_id = x.1; - if !minus_results.contains_key(&minus_id) { - minus_results.insert(minus_id, 0); - } + minus_results.entry(minus_id).or_insert(0); *minus_results.get_mut(&minus_id).unwrap() += 1; }); @@ -260,11 +257,10 @@ async fn get_votes(req: &Request, db: Arc>) -> Vec { let date = date.to_str().unwrap(); - let parsed_date = date.parse::(); - if parsed_date.is_err() { - None + if let Ok(parsed_date) = date.parse::() { + DateTime::from_timestamp_millis(parsed_date) } else { - DateTime::from_timestamp_millis(parsed_date.unwrap()) + None } } None => Some(DateTime::from(SystemTime::now())), @@ -273,25 +269,14 @@ async fn get_votes(req: &Request, db: Arc>) -> Vec Result, Error> { let authorised = is_authorised(req, db.clone(), 3).await; if !authorised { - return get_page(&req, "/unauthorised", db).await; + return get_page(req, "/unauthorised", db).await; } match path { "/admin" => get_page(req, path, db).await, @@ -320,38 +305,19 @@ async fn get_admin( } "/admin/players" => { let pool = db.clone().lock().unwrap().clone(); - let players = sqlx::query!(r#"SELECT id, name FROM players"#) + let players = sqlx::query_as!(Player, r#"SELECT id, name FROM players"#) .fetch_all(&pool) .await .unwrap(); - let players: Vec = players - .iter() - .map(|x| Player { - id: x.id, - name: x.name.clone(), - }) - .collect(); let stringed = serde_json::to_string(&players).unwrap_or("".to_string()); Ok(Response::builder().body(Body::new(stringed)).unwrap()) } "/admin/votes" => { let pool = db.clone().lock().unwrap().clone(); - let votes = sqlx::query!(r#"SELECT * FROM votes"#) + let votes = sqlx::query_as!(Vote, r#"SELECT * FROM votes"#) .fetch_all(&pool) .await .unwrap(); - let votes: Vec = votes - .iter() - .map(|x| Vote { - id: x.id, - plus_player_id: x.plus_player_id, - plus_nickname: x.plus_nickname.clone(), - plus_reason: x.plus_reason.clone(), - minus_player_id: x.minus_player_id, - minus_nickname: x.minus_nickname.clone(), - minus_reason: x.minus_reason.clone(), - }) - .collect(); let stringed = serde_json::to_string(&votes).unwrap_or("".to_string()); Ok(Response::builder().body(Body::new(stringed)).unwrap()) } @@ -402,7 +368,21 @@ async fn post_vote( .body(Body::Empty) .unwrap()); } - ok().await + let date: DateTime = DateTime::from(SystemTime::now()); + let date = date.checked_add_days(Days::new(1)).unwrap(); + let date = date + .with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()) + .unwrap(); + Ok(Response::builder() + .header( + SET_COOKIE, + format!( + "hasvoted=true; Expires={}; Secure; SameSite=Strict", + date.to_rfc2822() + ), + ) + .body(Body::Empty) + .unwrap()) } async fn post_player( @@ -550,16 +530,21 @@ async fn post_admin( }, "/admin/edit/vote" => match req_json::(req).await { Some(vote) => { + if vote.id.is_none() || vote.submit_date.is_none() { + return bad_request().await; + } let pool = db.clone().lock().unwrap().clone(); let _ = sqlx::query!( r#"UPDATE votes - SET plus_player_id = ?1, - plus_nickname = ?2, - plus_reason = ?3, - minus_player_id = ?4, - minus_nickname = ?5, - minus_reason = ?6 - WHERE id = ?7"#, + SET submit_date = ?1, + plus_player_id = ?2, + plus_nickname = ?3, + plus_reason = ?4, + minus_player_id = ?5, + minus_nickname = ?6, + minus_reason = ?7 + WHERE id = ?8"#, + vote.submit_date, vote.plus_player_id, vote.plus_nickname, vote.plus_reason, @@ -589,7 +574,7 @@ async fn post_admin( ok().await } _ => bad_request().await, - } + }, _ => bad_request().await, } } @@ -749,14 +734,11 @@ async fn is_authorised(req: &Request, db: Arc>, leve let perm = user.permissions as u8; perm >= level } - _ => match level { - 0 => true, - _ => false, - }, + _ => matches!(level, 0), } } -fn check_username(username: &String) -> bool { +fn check_username(username: &str) -> bool { if username.len() > 21 { return false; } @@ -768,7 +750,7 @@ fn check_username(username: &String) -> bool { true } -fn check_password(password: &String) -> bool { +fn check_password(password: &str) -> bool { // one symbol, 10 chars min, one capital letter, one number if password.len() < 10 { return false; diff --git a/static/html/index.html b/static/html/index.html index 6474231..51d73e8 100644 --- a/static/html/index.html +++ b/static/html/index.html @@ -11,7 +11,6 @@ -

Vote +

@@ -37,5 +36,6 @@ Résultats Archives
+ \ No newline at end of file diff --git a/static/js/index.js b/static/js/index.js index 4ce1531..8a98fb0 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,4 +1,5 @@ let vote = { + submit_date: null, plus_player_id: null, plus_nickname: "", plus_reason: "", @@ -11,7 +12,7 @@ let current_page = 0; async function main() { if (read_cookie()) { - showMessage("Merci pour ton vote!", "Ton vote a bien été prit en compte.", false, "info"); + showMessage("Merci pour ton vote!", "Ton vote a bien été pris en compte.", false, "info"); return; } let players = await fetch("/data/players").then(r => r.json()); @@ -95,7 +96,6 @@ async function main() { method: "post", body: JSON.stringify(vote) }) .then(r => r.status) === 200) { - set_cookie(); showMessage("Merci pour ton vote!", "Ton vote a bien été pris en compte.", false, "info"); } console.log(vote); @@ -160,14 +160,6 @@ function showMessage(title, description, canBeDismissed, type) { }) } -function set_cookie() { - let date = new Date(Date.now()); - date.setDate(date.getDate() + 1); - date.setHours(0, 0,0); - console.log(date); - document.cookie = `hasvoted=true; expires=${date.toUTCString()}; path=/`; -} - function read_cookie() { return document.cookie.includes("hasvoted=true"); } -- 2.47.3 From cbaf52256d69e7aa526689e2d80062dd1b6b33e6 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 5 Oct 2024 15:52:59 +0200 Subject: [PATCH 28/28] Remove unused import --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 5d7711a..429834e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use argon2::{ Argon2, }; use bytes::{Buf, Bytes}; -use chrono::{DateTime, Days, NaiveTime, Timelike, Utc}; +use chrono::{DateTime, Days, NaiveTime, Utc}; #[cfg(target_os = "linux")] use daemonize::Daemonize; use http_body_util::{BodyExt, Full}; -- 2.47.3