v1.0: First production version
This first version allows login of pre-existing users, creation and update of truths by admins, vote on the truths by users, their display as well as a simple graph for the vote results. Everything persisting in a SQLite database.
This commit is contained in:
commit
9911895b5b
22 changed files with 4790 additions and 0 deletions
201
src/auth.rs
Normal file
201
src/auth.rs
Normal file
|
@ -0,0 +1,201 @@
|
|||
use rocket::http::CookieJar;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::form::Form;
|
||||
use rocket::response::Redirect;
|
||||
use rocket_db_pools::{sqlx, sqlx::Row, Connection};
|
||||
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||
use blake2::{Blake2b512, Digest};
|
||||
use blake2::digest::FixedOutput;
|
||||
use sqlx::Error;
|
||||
use crate::database_records::{AuthTokens, PlayerLoginInfo, Vote};
|
||||
use crate::database;
|
||||
use database::Db;
|
||||
|
||||
// TODO: Make FromRequest guard https://api.rocket.rs/v0.5/rocket/request/trait.FromRequest and split admin
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct User {
|
||||
pub logged_in: bool,
|
||||
pub is_admin: bool,
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
pub has_week_vote: bool,
|
||||
pub votes: Vec<Vote>
|
||||
}
|
||||
|
||||
pub async fn get_user(week: u8, db: &mut Connection<Db>, cookies: &CookieJar<'_>) -> User {
|
||||
let auth_token: Option<String> = match cookies.get_private("auth_token") {
|
||||
Some(cookie) => Some(cookie.value().to_string()),
|
||||
None => None
|
||||
};
|
||||
let auth_id: Option<String> = match cookies.get_private("auth_id") {
|
||||
Some(cookie) => Some(cookie.value().to_string()),
|
||||
None => None
|
||||
};
|
||||
|
||||
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Non monotonous time event").as_secs();
|
||||
match sqlx::query("DELETE FROM AuthTokens WHERE max_timestamp < $1;")
|
||||
.bind(current_time as i64)
|
||||
.fetch_optional(&mut ***db).await {
|
||||
Ok(_) => debug!("Cleaned up old tokens"),
|
||||
Err(error) => error!("Error while cleaning up old tokens : {error}"),
|
||||
};
|
||||
|
||||
let id_str;
|
||||
|
||||
let mut logged_in: bool = if auth_token.is_some() && auth_id.is_some() {
|
||||
id_str = auth_id.unwrap().to_string();
|
||||
let token_str = auth_token.unwrap().to_string();
|
||||
let tokens: Vec::<AuthTokens> = match sqlx::query_as("SELECT token FROM AuthTokens WHERE player_id == $1")
|
||||
.bind(&id_str)
|
||||
.fetch_all(&mut ***db).await {
|
||||
Ok(auth_tokens) => {auth_tokens},
|
||||
Err(error) => {
|
||||
error!("Failed to fetch auth tokens for {:?} : {error}", &id_str);
|
||||
Vec::<AuthTokens>::new()
|
||||
}
|
||||
};
|
||||
tokens.iter().find(|auth_token| {println!("Token : {:?}\nCookie : {:?}", auth_token.token, token_str); auth_token.token.eq(&token_str)}).is_some()
|
||||
} else {
|
||||
id_str = String::new();
|
||||
false
|
||||
};
|
||||
// Consider the user authentified now, but revert in case of an error.
|
||||
|
||||
let (name, is_admin): (String, bool) = if logged_in {
|
||||
match sqlx::query("SELECT name, is_admin FROM Players WHERE id == $1")
|
||||
.bind(&id_str)
|
||||
.fetch_one(&mut ***db)
|
||||
.await {
|
||||
Ok(row) => match (row.try_get(0).ok().unwrap(), row.try_get(1).ok().unwrap()) {
|
||||
(Some(name), Some(admin)) => (name, admin),
|
||||
_ => {
|
||||
error!("Invalid return to retrieve user name and admin status");
|
||||
logged_in = false;
|
||||
(String::new(), false)
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
error!("Error while retrieving user name : {error}");
|
||||
logged_in = false;
|
||||
(String::new(), false)
|
||||
},
|
||||
}
|
||||
} else {
|
||||
(String::new(), false)
|
||||
};
|
||||
|
||||
let votes: Vec<Vote> = if logged_in {
|
||||
sqlx::query_as("SELECT Votes.* FROM Votes JOIN Truths ON Votes.truth_id == Truths.id AND Truths.week == $1 WHERE voter_id == $2 ORDER BY Truths.number;")
|
||||
.bind(week)
|
||||
.bind(&id_str)
|
||||
.fetch_all(&mut ***db).await.unwrap_or_else(|error| {
|
||||
error!("Error while getting votes : {error}");
|
||||
Vec::<Vote>::new()
|
||||
})
|
||||
} else {
|
||||
Vec::<Vote>::new()
|
||||
};
|
||||
|
||||
if logged_in {
|
||||
User {
|
||||
logged_in,
|
||||
is_admin,
|
||||
id: id_str.parse::<u16>().unwrap(),
|
||||
name,
|
||||
has_week_vote: if votes.is_empty() { false } else { true },
|
||||
votes
|
||||
}
|
||||
} else {
|
||||
User {
|
||||
logged_in,
|
||||
is_admin: false,
|
||||
id: 0,
|
||||
name,
|
||||
has_week_vote: false,
|
||||
votes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct AuthForm {
|
||||
name: String,
|
||||
password: String
|
||||
}
|
||||
|
||||
#[post("/login", data="<form>")]
|
||||
pub async fn login(form: Form<AuthForm>, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
|
||||
let user_search: Result<PlayerLoginInfo, _> = sqlx::query_as("SELECT id, is_admin, name, pwd_hash FROM Players WHERE name == $1")
|
||||
.bind(&form.name)
|
||||
.fetch_one(&mut **db)
|
||||
.await;
|
||||
|
||||
if user_search.is_err() {
|
||||
error!("Login failed : invalid user {:?}, err: {:?}", form.name, user_search.err());
|
||||
cookies.add(("toast_error", "Impossible de se connecter !"));
|
||||
return Redirect::to(uri!("/"));
|
||||
}
|
||||
let new_user = user_search.unwrap();
|
||||
|
||||
let password_hash_parse = PasswordHash::new(new_user.pwd_hash.as_str());
|
||||
if password_hash_parse.is_err() {
|
||||
error!("Login failed : could not parse password hash {:?}", password_hash_parse.err());
|
||||
cookies.add(("toast_error", "Impossible de se connecter !"));
|
||||
return Redirect::to(uri!("/"));
|
||||
}
|
||||
let password_hash = password_hash_parse.unwrap();
|
||||
|
||||
match Argon2::default().verify_password(form.password.as_bytes(), &password_hash) {
|
||||
Ok(_) => {
|
||||
let token_creation_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Non monotonous time event").as_secs();
|
||||
// Generate some kind of authentication token.
|
||||
let mut hasher = Blake2b512::new();
|
||||
hasher.update(new_user.id.to_le_bytes());
|
||||
hasher.update(token_creation_time.to_le_bytes());
|
||||
let hash = hasher.finalize_fixed().to_ascii_lowercase();
|
||||
let hash_str = String::from_utf8_lossy(hash.as_slice()).to_ascii_lowercase();
|
||||
|
||||
match sqlx::query("INSERT INTO AuthTokens (player_id, token, max_timestamp) VALUES ($1, $2, $3);")
|
||||
.bind(new_user.id)
|
||||
.bind(hash_str.clone())
|
||||
.bind((token_creation_time + 3628800) as i64) // Should be a while until we overflow to the 64th bit...
|
||||
.fetch_optional(&mut **db)
|
||||
.await {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
error!("Login failed : coult not store auth token in database : {error}");
|
||||
cookies.add(("toast_error", "Impossible de se connecter !"));
|
||||
return Redirect::to(uri!("/"));
|
||||
}
|
||||
}
|
||||
|
||||
cookies.add_private(("auth_token", hash_str.clone()));
|
||||
cookies.add_private(("auth_id", new_user.id.to_string()));
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Login failed : invalid password for {:?}\nError : {err}", new_user.name);
|
||||
cookies.add(("toast_error", "Impossible de se connecter !"));
|
||||
return Redirect::to(uri!("/"));
|
||||
}
|
||||
}
|
||||
|
||||
Redirect::to(uri!("/"))
|
||||
}
|
||||
|
||||
pub fn bypass_auth_debug(cookies: &CookieJar<'_>) {
|
||||
if cookies.get_private("auth_token").is_some() {
|
||||
return
|
||||
}
|
||||
let mut hasher = Blake2b512::new();
|
||||
hasher.update(b"8");
|
||||
hasher.update(SystemTime::now().duration_since(UNIX_EPOCH).expect("Non monotonous time event").as_secs().to_le_bytes());
|
||||
let hash = hasher.finalize_fixed().to_ascii_lowercase();
|
||||
let hash_str = String::from_utf8_lossy(hash.as_slice()).to_ascii_lowercase();
|
||||
cookies.add_private(("auth_token", hash_str.clone()));
|
||||
cookies.add_private(("auth_id", 8.to_string()));
|
||||
|
||||
println!("Generated hash string : {hash_str}");
|
||||
}
|
26
src/database.rs
Normal file
26
src/database.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use rocket::{Build, fairing, Rocket};
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket_db_pools::Database;
|
||||
|
||||
#[derive(Database)]
|
||||
#[database("data_sqlite")]
|
||||
pub struct Db(sqlx::SqlitePool);
|
||||
async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
|
||||
match Db::fetch(&rocket) {
|
||||
Some(db) => match sqlx::migrate!("./db").run(&**db).await {
|
||||
Ok(_) => Ok(rocket),
|
||||
Err(e) => {
|
||||
error!("Failed to initialize SQLx database: {}", e);
|
||||
Err(rocket)
|
||||
}
|
||||
}
|
||||
None => Err(rocket),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage() -> AdHoc {
|
||||
AdHoc::on_ignite("SQLx Stage", |rocket| async {
|
||||
rocket.attach(Db::init())
|
||||
.attach(AdHoc::try_on_ignite("SQLx Migrations", run_migrations))
|
||||
})
|
||||
}
|
60
src/database_records.rs
Normal file
60
src/database_records.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use rocket::serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Player {
|
||||
id: u16,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct PlayerLoginInfo {
|
||||
pub id: u16,
|
||||
pub is_admin: bool,
|
||||
pub name: String,
|
||||
pub pwd_hash: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Truth {
|
||||
id: u32,
|
||||
week: u8,
|
||||
number: u8,
|
||||
author_id: u16,
|
||||
rendered_text: String,
|
||||
raw_text: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct DisplayTruth {
|
||||
id: u32,
|
||||
number: u8,
|
||||
author_id: u16,
|
||||
rendered_text: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Vote {
|
||||
pub id: u32,
|
||||
pub truth_id: u32,
|
||||
pub voter_id: u16,
|
||||
pub voted_id: u16
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct VotingData {
|
||||
pub votes_for: String,
|
||||
pub truth_number: u8,
|
||||
pub votes: u8
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct AuthTokens {
|
||||
pub token: String,
|
||||
}
|
102
src/main.rs
Normal file
102
src/main.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
#[macro_use] extern crate rocket;
|
||||
|
||||
use rocket::{Rocket, Build, futures};
|
||||
use rocket::fs::{FileServer, relative};
|
||||
use rocket::http::CookieJar;
|
||||
use rocket::response::Redirect;
|
||||
use rocket::serde::{Serialize, Deserialize, json::Json};
|
||||
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
|
||||
use rocket_db_pools::{sqlx, sqlx::Row, Database, Connection};
|
||||
|
||||
mod database_records;
|
||||
mod auth;
|
||||
mod vote;
|
||||
mod truth;
|
||||
|
||||
mod database;
|
||||
use database::Db;
|
||||
use database_records::*;
|
||||
use auth::User;
|
||||
|
||||
#[get("/<week_number>")]
|
||||
async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Template {
|
||||
let user: User = auth::get_user(week_number, &mut db, cookies).await;
|
||||
|
||||
let other_players = if user.logged_in {
|
||||
match sqlx::query_as("SELECT id, name FROM Players WHERE id <> $1 AND is_admin == 0 ORDER BY name")
|
||||
.bind(user.id)
|
||||
.fetch_all(&mut **db).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
println!("Some error while getting players : {error}");
|
||||
Vec::<Player>::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vec::<Player>::new()
|
||||
};
|
||||
|
||||
// FIXME : This is fucking *trash* but fucking hell mate
|
||||
if user.is_admin {
|
||||
let truths: Vec<Truth> = match sqlx::query_as("SELECT * FROM Truths WHERE week == $1 ORDER BY number")
|
||||
.bind(week_number)
|
||||
.fetch_all(&mut **db).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
error!("Error while getting truths : {error}");
|
||||
Vec::<Truth>::new()
|
||||
}
|
||||
};
|
||||
|
||||
Template::render("index", context! {
|
||||
week_number: week_number,
|
||||
truths: truths,
|
||||
user: user,
|
||||
other_players: other_players,
|
||||
})
|
||||
} else {
|
||||
let truths: Vec<DisplayTruth> = match sqlx::query_as("SELECT id, number, author_id, rendered_text FROM Truths WHERE week == $1 ORDER BY number")
|
||||
.bind(week_number)
|
||||
.fetch_all(&mut **db).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
error!("Error while getting truths : {error}");
|
||||
Vec::<DisplayTruth>::new()
|
||||
}
|
||||
};
|
||||
|
||||
Template::render("index", context! {
|
||||
week_number: week_number,
|
||||
truths: truths,
|
||||
user: user,
|
||||
other_players: other_players,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index(mut db: Connection<Db>) -> Redirect {
|
||||
let current_week: u8 = match sqlx::query("SELECT max(week) AS last_week FROM Truths;")
|
||||
.fetch_one(&mut **db).await {
|
||||
Ok(v) => v.try_get(0).ok().unwrap_or_else(|| 1), // If error, go back to 1
|
||||
Err(error) => {
|
||||
error!("Error while getting current week : {error:?}");
|
||||
1
|
||||
}
|
||||
};
|
||||
|
||||
Redirect::to(uri!("/", week(week_number = if current_week == 0 {1} else {current_week})))
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
rocket::build()
|
||||
.mount("/", FileServer::from(relative!("static_files")))
|
||||
.mount("/", routes![index, vote::fetch_vote_data, vote::vote, truth::create_truth, truth::edit_truth, week, auth::login])
|
||||
.attach(database::stage())
|
||||
.attach(Template::fairing())
|
||||
}
|
||||
|
||||
// TODO: Random Row ID
|
112
src/truth.rs
Normal file
112
src/truth.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use rocket::form::Form;
|
||||
use rocket::http::CookieJar;
|
||||
use rocket::response::Redirect;
|
||||
|
||||
use rocket_db_pools::{sqlx, Connection};
|
||||
|
||||
use pulldown_cmark::{Parser, Options};
|
||||
use sqlx::Row;
|
||||
use crate::{auth, database};
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct TruthUpdateForm {
|
||||
truth_raw_text: String,
|
||||
truth_author: u8,
|
||||
}
|
||||
|
||||
#[post("/<week>/edit/<truth_number>", data="<form>")]
|
||||
pub async fn edit_truth(week: u8, truth_number: u8, form: Form<TruthUpdateForm>,
|
||||
mut db: Connection<database::Db>, cookies: &CookieJar<'_>) -> Redirect {
|
||||
let user = auth::get_user(week, &mut db, cookies).await;
|
||||
if !user.is_admin {
|
||||
cookies.add(("toast_error", "Vous n'avez pas la permission de changer la vérité."));
|
||||
return Redirect::to(uri!("/"));
|
||||
}
|
||||
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
options.insert(Options::ENABLE_FOOTNOTES);
|
||||
options.insert(Options::ENABLE_MATH);
|
||||
options.insert(Options::ENABLE_TABLES);
|
||||
let markdown_parser = Parser::new_ext(form.truth_raw_text.as_str(), options);
|
||||
|
||||
let mut rendered_markdown = String::new();
|
||||
pulldown_cmark::html::push_html(&mut rendered_markdown, markdown_parser);
|
||||
|
||||
match sqlx::query("UPDATE Truths SET raw_text = $1, rendered_text = $2, author_id = $3 WHERE week == $4 AND number == $5;")
|
||||
.bind(&form.truth_raw_text)
|
||||
.bind(rendered_markdown)
|
||||
.bind(form.truth_author)
|
||||
.bind(week)
|
||||
.bind(truth_number)
|
||||
.fetch_optional(&mut **db)
|
||||
.await {
|
||||
Ok(_) => {
|
||||
debug!("Truth successfully updated")
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Error while updating truth {truth_number} from week {week} : {error}");
|
||||
cookies.add(("toast_error", "Il y a eu un problème lors du changement de la vérité"));
|
||||
}
|
||||
};
|
||||
|
||||
Redirect::to(uri!("/"))
|
||||
}
|
||||
|
||||
#[post("/<week>/new", data="<form>")]
|
||||
pub async fn create_truth(week: u8, form: Form<TruthUpdateForm>,
|
||||
mut db: Connection<database::Db>, cookies: &CookieJar<'_>) -> Redirect {
|
||||
let user = auth::get_user(week, &mut db, cookies).await;
|
||||
if !user.is_admin {
|
||||
cookies.add(("toast_error", "Vous n'avez pas la permission d'ajouter de vérité."));
|
||||
return Redirect::to(uri!("/"));
|
||||
}
|
||||
|
||||
let truth_number: u8 = match sqlx::query("SELECT max(number) from Truths WHERE week == $1;")
|
||||
.bind(week)
|
||||
.fetch_one(&mut **db)
|
||||
.await {
|
||||
Ok(row) => match row.try_get::<u8, usize>(0).ok() {
|
||||
Some(max_truth_number) => max_truth_number+1,
|
||||
None => 0
|
||||
},
|
||||
Err(_) => 1 // If we can't fetch a row, this is the first one.
|
||||
};
|
||||
|
||||
if truth_number == 0 {
|
||||
error!("Error while getting max truth number.");
|
||||
cookies.add(("toast_error", "Erreur lors de l'ajout de la vérité..."));
|
||||
return Redirect::to(uri!("/"));
|
||||
}
|
||||
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
options.insert(Options::ENABLE_FOOTNOTES);
|
||||
options.insert(Options::ENABLE_MATH);
|
||||
options.insert(Options::ENABLE_TABLES);
|
||||
let markdown_parser = Parser::new_ext(form.truth_raw_text.as_str(), options);
|
||||
|
||||
let mut rendered_markdown = String::new();
|
||||
pulldown_cmark::html::push_html(&mut rendered_markdown, markdown_parser);
|
||||
|
||||
match sqlx::query("INSERT INTO Truths (week, number, rendered_text, raw_text, author_id) VALUES ($1, $2, $3, $4, $5);")
|
||||
.bind(week)
|
||||
.bind(truth_number)
|
||||
.bind(rendered_markdown)
|
||||
.bind(&form.truth_raw_text)
|
||||
.bind(form.truth_author)
|
||||
.fetch_optional(&mut **db)
|
||||
.await {
|
||||
Ok(_) => {
|
||||
debug!("Truth successfully updated")
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Error while creating truth {truth_number} from week {week} : {error}");
|
||||
cookies.add(("toast_error", "Il y a eu un problème lors du changement de la vérité"));
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Truth was successfully added");
|
||||
|
||||
Redirect::to(uri!("/"))
|
||||
}
|
150
src/vote.rs
Normal file
150
src/vote.rs
Normal file
|
@ -0,0 +1,150 @@
|
|||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket::form::Form;
|
||||
use rocket::http::CookieJar;
|
||||
use rocket::response::Redirect;
|
||||
use rocket::serde::{Serialize, Deserialize};
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
use rocket_db_pools::{sqlx, Connection};
|
||||
|
||||
use crate::{auth, database};
|
||||
use crate::database::Db;
|
||||
use crate::database_records::{Vote, VotingData};
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct VoteForm {
|
||||
truth_votes: HashMap<u32, u16>
|
||||
}
|
||||
|
||||
#[post("/<week>/vote", data="<form>")]
|
||||
pub async fn vote(week: u8, form: Form<VoteForm>,
|
||||
mut db: Connection<database::Db>, cookies: &CookieJar<'_>) -> Redirect {
|
||||
let user = auth::get_user(week, &mut db, cookies).await;
|
||||
|
||||
if !user.logged_in {
|
||||
cookies.add(("toast_error", "Vous n'avez pas la permission de changer de vote."));
|
||||
return Redirect::to(uri!("/"));
|
||||
}
|
||||
|
||||
let filtered_votes = form.truth_votes.iter().filter_map(
|
||||
|vote| {
|
||||
if *vote.1 != 0 {
|
||||
Some(vote)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let mut had_error = false;
|
||||
for (truth_id, voted_id) in filtered_votes {
|
||||
match user.votes.iter().find(|vote: &&Vote| {vote.truth_id == *truth_id}) {
|
||||
Some(vote) => {
|
||||
if *voted_id == vote.voted_id {
|
||||
continue;
|
||||
}
|
||||
debug!("Player {:?} updating vote {:?} for truth {truth_id} : \n\t\
|
||||
Previously voted {:?}, now voted {voted_id}", user.id, vote.id, vote.voted_id);
|
||||
match sqlx::query("UPDATE Votes SET voted_id = $3 WHERE truth_id == $1 AND voter_id == $2;")
|
||||
.bind(truth_id)
|
||||
.bind(user.id)
|
||||
.bind(voted_id)
|
||||
.fetch_optional(&mut **db)
|
||||
.await {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
error!("Error while submitting a vote : {error}");
|
||||
had_error = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
None => {
|
||||
debug!("Player {:?} voting {voted_id} for truth {truth_id}", user.id);
|
||||
// TODO: Find a way to use only one statement ?
|
||||
// Cannot all launch and await because all connect to DB
|
||||
match sqlx::query("INSERT INTO Votes (truth_id, voter_id, voted_id) VALUES ($1, $2, $3);")
|
||||
.bind(truth_id)
|
||||
.bind(user.id)
|
||||
.bind(voted_id)
|
||||
.fetch_optional(&mut **db)
|
||||
.await {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
error!("Error while submitting a vote : {error}");
|
||||
had_error = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if had_error {
|
||||
cookies.add(("toast_error", "Il y a eu un problème lors de la soumission du vote !"));
|
||||
} else {
|
||||
debug!("Vote successful")
|
||||
}
|
||||
|
||||
Redirect::to(uri!("/"))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct VoteData {
|
||||
votes: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
// TODO: Cache vote count ? Maintain in state ?
|
||||
#[get("/<week>/votes", format = "application/json")]
|
||||
pub async fn fetch_vote_data(week: u8, mut db: Connection<database::Db>) -> Option<Json<VoteData>> {
|
||||
let raw_votes: Vec<VotingData> = sqlx::query_as("
|
||||
SELECT Players.name as votes_for, Truths.number as truth_number, count(*) as votes FROM Votes
|
||||
JOIN Players ON Votes.voted_id == Players.id
|
||||
JOIN Truths on Votes.truth_id == Truths.id AND Truths.week == $1
|
||||
GROUP BY votes_for, truth_number
|
||||
ORDER BY votes_for, truth_number;")
|
||||
.bind(week)
|
||||
.fetch_all(&mut **db)
|
||||
.await.unwrap_or_else(|error| {
|
||||
error!("Error while fetching vote data {error}");
|
||||
Vec::<VotingData>::new()
|
||||
});
|
||||
|
||||
let vote_count = raw_votes.iter().fold(0, |count, votes| {count + votes.votes});
|
||||
if vote_count < 17 {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
let mut vote_data = HashMap::<String, Vec<u8>>::new();
|
||||
let mut next_truth_number = 1;
|
||||
for raw_vote in raw_votes {
|
||||
// The truth_number is monotonous (sorted by the SQL request).
|
||||
// If the next vote's truth is lower, we have changed player.
|
||||
if next_truth_number > raw_vote.truth_number {
|
||||
next_truth_number = 1;
|
||||
}
|
||||
|
||||
let votes_for_player: &mut Vec<u8> = match vote_data.entry(raw_vote.votes_for) {
|
||||
Entry::Occupied(existing) => {existing.into_mut()}
|
||||
Entry::Vacant(vacant) => {vacant.insert(Vec::<u8>::new())}
|
||||
};
|
||||
|
||||
// Fill up missing spaces if we are missing any.
|
||||
for _ in next_truth_number..raw_vote.truth_number {
|
||||
votes_for_player.push(0);
|
||||
}
|
||||
// Update with the vote result.
|
||||
votes_for_player.push(raw_vote.votes);
|
||||
next_truth_number = raw_vote.truth_number + 1;
|
||||
}
|
||||
|
||||
Some(Json(VoteData{votes: vote_data}))
|
||||
}
|
||||
|
||||
// FIXME:
|
||||
pub fn stage() -> AdHoc {
|
||||
AdHoc::on_ignite("SQLx Stage", |rocket| async {
|
||||
rocket.mount("/", routes![vote, fetch_vote_data])
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue