diff --git a/Cargo.lock b/Cargo.lock index b3eece5..df623d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -578,7 +578,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fabula_votes_server" -version = "1.4.1" +version = "1.2.1" dependencies = [ "argon2", "blake2", diff --git a/Cargo.toml b/Cargo.toml index 298d596..2ec545f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "fabula_votes_server" license = "MPL-2.0" readme = "README.md" authors = ["trotFunky"] -version = "1.4.1" +version = "1.2.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index c2e95a2..2b6d940 100644 --- a/README.md +++ b/README.md @@ -16,23 +16,18 @@ A list of things that could be implemented/added to the application, some of the - [ ] Move the database queries to their own functions - [ ] Cache those results - [ ] Centralize Markdown parsing ? - - [x] Use fairings for the different elements + - [ ] Use fairings for the different elements ? - [ ] Use guards for User calls ? - - [ ] Use SQLite Row ID for User IDs rather than regular IDs, for randomness ? - - [x] Split user from vote data # Dependencies -This project currently uses, for the backend : +This project currently uses : - [Rocket](https://docs.rs/rocket/0.5.1/rocket/), for the web application backend - [SQLX](https://docs.rs/sqlx/0.7.4/sqlx/), for database access (this is only expeceted to be used with SQLite) - [Tera](https://docs.rs/tera/latest/tera/), for templating - [Argon2](https://docs.rs/argon2/latest/argon2/), for password hashing - [Pull_down CMark](https://docs.rs/pulldown-cmark/0.11.0/pulldown_cmark/), for markdown rendering -For the frontend : - - [Chart.js](https://www.chartjs.org/), for rendering the vote graph. - # License The code present in this repository is licensed under the Mozilla Public License 2.0. diff --git a/db/04_create-tags-tables.sql b/db/04_create-tags-tables.sql deleted file mode 100644 index e7bb714..0000000 --- a/db/04_create-tags-tables.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE IF NOT EXISTS Tags ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR NOT NULL UNIQUE, - color VARCHAR NOT NULL -); - -CREATE TABLE IF NOT EXISTS TaggedTruths ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - truth_id INTEGER NOT NULL, - tag_id INTEGER NOT NULL, - FOREIGN KEY (truth_id) REFERENCES Truths(id) ON DELETE CASCADE, - FOREIGN KEY (tag_id) REFERENCES Tags(id) ON DELETE CASCADE - ); diff --git a/src/auth.rs b/src/auth.rs index 672b79d..7f6b251 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -8,8 +8,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use argon2::{Argon2, PasswordHash, PasswordVerifier}; use blake2::{Blake2b512, Digest}; use blake2::digest::FixedOutput; -use rocket::fairing::AdHoc; -use crate::database_records::{AuthTokens, PlayerLoginInfo}; +use crate::database_records::{AuthTokens, PlayerLoginInfo, Vote}; use crate::{database, week}; use database::Db; @@ -21,9 +20,11 @@ pub struct User { pub is_admin: bool, pub id: u16, pub name: String, + pub has_week_vote: bool, + pub votes: Vec } -pub async fn get_user(db: &mut Connection, cookies: &CookieJar<'_>) -> User { +pub async fn get_user(week: u8, db: &mut Connection, cookies: &CookieJar<'_>) -> User { let auth_token: Option = match cookies.get_private("auth_token") { Some(cookie) => Some(cookie.value().to_string()), None => None @@ -85,12 +86,27 @@ pub async fn get_user(db: &mut Connection, cookies: &CookieJar<'_>) -> User (String::new(), false) }; + // TODO: Move to src/vote.rs + let votes: Vec = if logged_in && !is_admin { + 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::::new() + }) + } else { + Vec::::new() + }; + if logged_in { User { logged_in, is_admin, id: id_str.parse::().unwrap(), name, + has_week_vote: if votes.is_empty() { false } else { true }, + votes } } else { User { @@ -98,6 +114,8 @@ pub async fn get_user(db: &mut Connection, cookies: &CookieJar<'_>) -> User is_admin: false, id: 0, name, + has_week_vote: false, + votes } } } @@ -167,34 +185,17 @@ pub async fn login(week: u8, form: Form, mut db: Connection, cooki Redirect::to(uri!(week::week(week))) } -#[post("//logout")] -pub async fn logout(week: u8, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let auth_token: Option = match cookies.get_private("auth_token") { - Some(cookie) => Some(cookie.value().to_string()), - None => None - }; - - // Should not be able to log out ? - if auth_token.is_none() { - return Redirect::to(uri!(week::week(week))) +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())); - match sqlx::query("DELETE FROM AuthTokens WHERE token == $1;") - .bind(auth_token) - .execute(&mut **db) - .await { - Ok(_) => debug!("Auth token deletion successful"), - Err(error) => debug!("Auth token could not be removed ({error}), proceeding anyway.") - } - - cookies.remove_private("auth_token"); - cookies.remove_private("auth_id"); - - Redirect::to(uri!(week::week(week))) -} - -pub fn stage() -> AdHoc { - AdHoc::on_ignite("Auth stage", |rocket| async { - rocket.mount("/", routes![login, logout]) - }) -} + println!("Generated hash string : {hash_str}"); +} \ No newline at end of file diff --git a/src/database_records.rs b/src/database_records.rs index 2f49061..b647a2c 100644 --- a/src/database_records.rs +++ b/src/database_records.rs @@ -19,26 +19,21 @@ pub struct PlayerLoginInfo { #[derive(sqlx::FromRow, Deserialize, Serialize)] #[serde(crate = "rocket::serde")] pub struct Truth { - pub id: u32, + id: u32, week: u8, number: u8, author_id: u16, rendered_text: String, raw_text: String, - #[sqlx(skip)] - pub tags: Vec, } #[derive(sqlx::FromRow, Deserialize, Serialize)] #[serde(crate = "rocket::serde")] pub struct DisplayTruth { - pub id: u32, - week: u8, + id: u32, number: u8, author_id: u16, rendered_text: String, - #[sqlx(skip)] - pub tags: Vec, } #[derive(sqlx::FromRow, Deserialize, Serialize)] @@ -72,11 +67,3 @@ pub struct Week { pub rendered_text: String, pub raw_text: String, } - -#[derive(FromForm)] -#[derive(sqlx::FromRow, Deserialize, Serialize)] -#[serde(crate = "rocket::serde")] -pub struct Tag { - pub name: String, - pub color: String -} diff --git a/src/main.rs b/src/main.rs index bf2299c..655d9af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,19 @@ #[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; +use rocket_dyn_templates::{Template, context}; -use rocket_db_pools::Connection; +use rocket_db_pools::{sqlx, sqlx::Row, Database, Connection}; +use sqlx::Error; mod auth; -mod tag; +use auth::User; + mod truth; mod vote; mod week; @@ -16,11 +21,19 @@ mod week; mod database; mod database_records; +use database_records::*; use database::Db; #[get("/")] async fn index(mut db: Connection) -> Redirect { - let current_week: u8 = week::get_last_week(&mut db).await; + let current_week: u8 = match sqlx::query("SELECT number FROM Weeks WHERE is_last_week == 1;") + .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(week_number = if current_week == 0 {1} else {current_week}))) } @@ -28,13 +41,14 @@ async fn index(mut db: Connection) -> Redirect { #[launch] fn rocket() -> _ { rocket::build() - .mount("/static/", FileServer::from(relative!("static_files"))) - .attach(auth::stage()) - .attach(week::stage()) - .attach(truth::stage()) - .attach(vote::stage()) - .attach(tag::stage()) - .mount("/", routes![index]) + .mount("/", FileServer::from(relative!("static_files"))) + .mount("/", routes![index, + vote::fetch_vote_data, vote::vote, + truth::create_truth, truth::edit_truth, + week::week, week::update_week, week::set_last_week, week::create_week, + auth::login]) .attach(database::stage()) .attach(Template::fairing()) } + +// TODO: Random Row ID \ No newline at end of file diff --git a/src/tag.rs b/src/tag.rs deleted file mode 100644 index 1069e36..0000000 --- a/src/tag.rs +++ /dev/null @@ -1,295 +0,0 @@ -use rocket::fairing::AdHoc; -use rocket::form::Form; -use rocket::http::CookieJar; -use rocket::response::Redirect; -use rocket_db_pools::Connection; -use rocket_dyn_templates::{context, Template}; -use sqlx::{Sqlite, QueryBuilder}; -use crate::database::Db; -use crate::database_records::{DisplayTruth, Tag}; - -use crate::auth; -use crate::week; - -pub async fn get_all_tags(db: &mut Connection, cookies: &CookieJar<'_>) -> Vec { - match sqlx::query_as("SELECT name, color FROM Tags") - .fetch_all(&mut ***db) - .await { - Ok(tags) => tags, - Err(error) => { - error!("Database error while fetching tags : {error}"); - cookies.add(("toast_error", "Erreur lors du chargement des tags.")); - Vec::::new() - } - } -} - -pub async fn get_truth_tags(truth_id: u32, db: &mut Connection) -> Vec { - match sqlx::query_as(" - SELECT Tags.name, Tags.color FROM Tags - JOIN TaggedTruths ON TaggedTruths.tag_id == Tags.id AND TaggedTruths.truth_id == $1 - ORDER BY Tags.id;") - .bind(truth_id) - .fetch_all(&mut ***db) - .await { - Ok(tags) => tags, - Err(error) => { - error!("Could not fetch tags for truth {:?} : {error}", truth_id); - Vec::::new() - } - } -} - -#[get("/")] -pub async fn tag_index(mut db: Connection, cookies: &CookieJar<'_>) -> Template { - let user = auth::get_user(&mut db, cookies).await; - - let tags: Vec = get_all_tags(&mut db, cookies).await; - - Template::render("tags/index", context!{ - user: user, - tags: tags - }) -} - -#[get("/filter?")] -pub async fn filtered_by_tags(tags: Vec, mut db: Connection) -> Template { - let last_week = week::get_last_week(&mut db).await; - let filtered_truths: Vec = if tags.len() == 0 { - match sqlx::query_as("SELECT id, week, number, author_id, rendered_text FROM Truths WHERE week <= $1 ORDER BY week, number;") - .bind(last_week) - .fetch_all(&mut **db) - .await { - Ok(all_truths) => all_truths, - Err(error) => { - error!("Error while fetching all truths for the filter : {error}"); - Vec::::new() - } - } - } else { - let mut query_builder: QueryBuilder = QueryBuilder::new(" - SELECT Truths.id, Truths.week, Truths.number, Truths.author_id, Truths.rendered_text FROM Truths - JOIN TaggedTruths ON Truths.id == TaggedTruths.truth_id - JOIN Tags ON Tags.id == TaggedTruths.tag_id AND Tags.name IN ( - "); - let mut separated_args = query_builder.separated(", "); - for tag in &tags { - separated_args.push_bind(tag); - } - - // Now that the comma separated list of strings is built, finish the query normally. - query_builder.push(") WHERE Truths.week <= "); - query_builder.push_bind(last_week); - query_builder.push("GROUP BY Truths.id ORDER BY Truths.week, Truths.number;"); - match query_builder.build_query_as::().fetch_all(&mut **db).await { - Ok(truths) => { - debug!("Got filtered truths by tags"); - truths - }, - Err(error) => { - error!("Error while fetching filtered truths : {error}"); - Vec::::new() - } - } - }; - - let filter_tags: Vec:: = match sqlx::query_as("SELECT name, color FROM Tags;") - .fetch_all(&mut **db) - .await { - Ok(all_tags) => { - all_tags.into_iter().filter(|tag: &Tag| tags.contains(&tag.name)).collect() - }, - Err(error) => { - error!("Error getting tags to show on filter list : {error}"); - Vec::::new() - } - }; - - Template::render("tags/filtered_truths", context!{ - tags: filter_tags, - truths: filtered_truths - }) -} - - -#[post("/create", data="")] -pub async fn create_tag(new_tag: Form, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; - if !user.is_admin { - cookies.add(("toast_error", "Vous n'avez pas la permission de créer une étiquette")); - return Redirect::to(uri!("/tags", tag_index)); - } - - match sqlx::query("INSERT INTO Tags (name, color) VALUES ($1, $2);") - .bind(&new_tag.name) - .bind(&new_tag.color) - .execute(&mut **db) - .await { - Ok(affected_lines) => if affected_lines.rows_affected() != 1 { - error!("Did not create tag : {:?}", &new_tag.name); - cookies.add(("toast_error", "L'étiquette n'a pas pû être créer")); - } else { - debug!("Successfully created new tag {:?}", &new_tag.name); - }, - Err(error) => { - error!("Error whilea adding tag {:?} : {error}", &new_tag.name); - cookies.add(("toast_error", "Erreur à la création de l'étiquette")); - } - }; - - Redirect::to(uri!("/tags", tag_index)) -} - -#[post("/delete/")] -pub async fn delete_tag(tag_name: &str, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; - if !user.is_admin { - cookies.add(("toast_error", "Vous n'avez pas la permission de supprimer une étiquette")); - return Redirect::to(uri!("/tags", tag_index)); - } - - match sqlx::query("DELETE FROM Tags WHERE name == $1;") - .bind(&tag_name) - .execute(&mut **db) - .await { - Ok(affected_lines) => if affected_lines.rows_affected() != 1 { - error!("Did not remove {tag_name}"); - cookies.add(("toast_error", "L'étiquette n'a pas pû être supprimée.")); - } - else { - debug!("Tag {tag_name} successfully removed.") - }, - Err(error) => { - error!("Error trying to remove {tag_name} : {error}"); - cookies.add(("toast_error", "Erreur lors de la suppression de l'étiquette")); - } - }; - - Redirect::to(uri!("/tags", tag_index)) -} - -#[post("/update/", data="")] -pub async fn update_tag(tag_name: &str, updated_tag: Form, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; - if !user.is_admin { - cookies.add(("toast_error", "Vous n'avez pas la permission de modifier une étiquette")); - return Redirect::to(uri!("/tags", tag_index)); - } - - match sqlx::query("UPDATE Tags SET name = $1, color = $2 WHERE name == $3;") - .bind(&updated_tag.name) - .bind(&updated_tag.color) - .bind(&tag_name) - .execute(&mut **db) - .await { - Ok(affected_lines) => if affected_lines.rows_affected() != 1 { - error!("Did not update tag {tag_name}"); - cookies.add(("toast_error", "L'étiquette n'a pas pû être modifiée.")); - } else { - debug!("Updated {tag_name} successfully."); - } - Err(error) => { - error!("Error while updating {tag_name} : {error}"); - cookies.add(("toast_error", "Erreur lors de la modification de l'étiquette.")); - } - }; - - Redirect::to(uri!("/tags", tag_index)) -} - -#[derive(FromForm)] -pub struct TagForm { - name: String, -} - -#[post("//tag/", data="")] -pub async fn tag_truth(week: u8, truth_id: u32, tag_form: Form, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; - if !user.is_admin { - cookies.add(("toast_error", "Vous n'avez pas la permission d'ajouter une étiquette")); - return Redirect::to(uri!("/tags", tag_index)); - } - - let error_cookie = || - cookies.add(("toast_error", "Erreur lors de l'ajout de l'étiquette.")); - - let tag_id: u32 = sqlx::query_scalar("SELECT id FROM Tags WHERE name == $1;") - .bind(&tag_form.name) - .fetch_one(&mut **db) - .await.unwrap_or(0); - - if tag_id == 0 { - error_cookie(); - error!("Error while trying to figure tag ID of {:?} to create the relation with {:?}.", &tag_form.name, &truth_id); - return Redirect::to(uri!(week::week(week))); - } - - match sqlx::query("INSERT INTO TaggedTruths (truth_id, tag_id) VALUES ($1, $2);") - .bind(&truth_id) - .bind(&tag_id) - .execute(&mut **db) - .await { - Ok(affected_lines) => if affected_lines.rows_affected() != 1 { - error_cookie(); - error!("Tag relation not added."); - } else { - debug!("Created tag relation with truth {truth_id} and tag {tag_id}"); - } - Err(error) => { - error_cookie(); - error!("Error while trying to add tag relation : {error}"); - } - } - - Redirect::to(uri!(week::week(week))) -} - -#[post("//untag/", data="")] -pub async fn untag_truth(week: u8, truth_id: u32, tag_form: Form, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; - if !user.is_admin { - cookies.add(("toast_error", "Vous n'avez pas la permission de retirer une étiquette")); - return Redirect::to(uri!("/tags", tag_index)); - } - - let error_cookie = || - cookies.add(("toast_error", "Erreur lors de la suppression de l'étiquette.")); - - let tag_id: u32 = sqlx::query_scalar("SELECT id FROM Tags WHERE name == $1;") - .bind(&tag_form.name) - .fetch_one(&mut **db) - .await.unwrap_or(0); - - if tag_id == 0 { - error_cookie(); - error!("Error while trying to figure tag ({:?}) ID to remove the relation with {:?}.", &tag_id, &truth_id); - return Redirect::to(uri!(week::week(week))); - } - - match sqlx::query("DELETE FROM TaggedTruths WHERE truth_id == $1 AND tag_id == $2;") - .bind(&truth_id) - .bind(&tag_id) - .execute(&mut **db) - .await { - Ok(affected_lines) => if affected_lines.rows_affected() != 1 { - error_cookie(); - error!("Tag relation not deleted."); - } else { - debug!("Removed tag relation with truth {truth_id} and tag {tag_id}"); - } - Err(error) => { - error_cookie(); - error!("Error while trying to remove tag relation : {error}"); - } - } - - Redirect::to(uri!(week::week(week))) -} - -pub fn stage() -> AdHoc { - AdHoc::on_ignite("Tag stage", |rocket| async { - rocket - .mount("/tags", routes![tag_index, filtered_by_tags, create_tag, update_tag, delete_tag]) - .mount("/", routes![tag_truth, untag_truth]) - }) -} diff --git a/src/truth.rs b/src/truth.rs index f3b6add..861449c 100644 --- a/src/truth.rs +++ b/src/truth.rs @@ -5,7 +5,6 @@ use rocket::response::Redirect; use rocket_db_pools::{sqlx, Connection}; use pulldown_cmark::{Parser, Options}; -use rocket::fairing::AdHoc; use sqlx::Row; use crate::{auth, database, week}; @@ -18,7 +17,7 @@ pub struct TruthUpdateForm { #[post("//edit/", data="
")] pub async fn edit_truth(week: u8, truth_number: u8, form: Form, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; + 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!(week::week(week))); @@ -57,7 +56,7 @@ pub async fn edit_truth(week: u8, truth_number: u8, form: Form, #[post("//new_truth", data="")] pub async fn create_truth(week: u8, form: Form, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; + 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!(week::week(week))); @@ -111,9 +110,3 @@ pub async fn create_truth(week: u8, form: Form, Redirect::to(uri!(week::week(week))) } - -pub fn stage() -> AdHoc { - AdHoc::on_ignite("Truth stage", |rocket| async { - rocket.mount("/", routes![create_truth, edit_truth]) - }) -} diff --git a/src/vote.rs b/src/vote.rs index 550707c..6795167 100644 --- a/src/vote.rs +++ b/src/vote.rs @@ -2,6 +2,7 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use rocket::fairing::AdHoc; use rocket::form::Form; +use rocket::futures::TryFutureExt; use rocket::http::CookieJar; use rocket::response::Redirect; use rocket::serde::{Serialize, Deserialize}; @@ -12,29 +13,6 @@ use rocket_db_pools::{sqlx, Connection}; use crate::{auth, database, week}; use crate::database_records::{Vote, VotingData}; -#[derive(Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct WeeklyUserVotes { - pub has_week_vote: bool, - pub votes: Vec -} - -pub async fn get_weekly_user_votes(week: u8, user: &auth::User, db: &mut Connection) -> WeeklyUserVotes { - let votes: Vec = if user.logged_in && !user.is_admin { - 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(user.id) - .fetch_all(&mut ***db).await.unwrap_or_else(|error| { - error!("Error while getting votes : {error}"); - Vec::::new() - }) - } else { - Vec::::new() - }; - - WeeklyUserVotes {has_week_vote: if votes.is_empty() { false } else { true }, votes} -} - #[derive(FromForm)] pub struct VoteForm { truth_votes: HashMap @@ -43,7 +21,7 @@ pub struct VoteForm { #[post("//vote", data="")] pub async fn vote(week: u8, form: Form, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; + 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.")); @@ -60,11 +38,9 @@ pub async fn vote(week: u8, form: Form, } ); - let existing_votes = get_weekly_user_votes(week, &user, &mut db).await; - let mut had_error = false; for (truth_id, voted_id) in filtered_votes { - match existing_votes.votes.iter().find(|vote: &&Vote| {vote.truth_id == *truth_id}) { + match user.votes.iter().find(|vote: &&Vote| {vote.truth_id == *truth_id}) { Some(vote) => { if *voted_id == vote.voted_id { continue; @@ -115,14 +91,12 @@ pub async fn vote(week: u8, form: Form, #[derive(Serialize, Deserialize)] #[serde(crate = "rocket::serde")] pub struct VoteData { - truth_count: u8, votes: HashMap>, } // TODO: Cache vote count ? Maintain in state ? #[get("//votes", format = "application/json")] -pub async fn fetch_vote_data(week: u8, mut db: Connection, cookies: &CookieJar<'_>) -> Option> { - let user = auth::get_user(&mut db, cookies).await; +pub async fn fetch_vote_data(week: u8, mut db: Connection) -> Option> { let raw_votes: Vec = 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 @@ -136,25 +110,12 @@ pub async fn fetch_vote_data(week: u8, mut db: Connection, cookies Vec::::new() }); - let truth_count: u8 = sqlx::query_scalar("SELECT count(id) from Truths WHERE week == $1;") - .bind(week) - .fetch_one(&mut **db) - .await.unwrap_or(0); - - let player_count: u8 = sqlx::query_scalar("SELECT count(id) from Players WHERE is_admin == 0;") - .fetch_one(&mut **db) - .await.unwrap_or(0); - - // Each player should have a truth assigned to them which they cannot vote on. - let max_vote_count = truth_count * (player_count - 1); - let vote_count = raw_votes.iter().fold(0, |count, votes| {count + votes.votes}); // Only show the graph if we have all the votes and this is not the last week. - if !user.is_admin && (max_vote_count == 0 - || vote_count < max_vote_count - || week == sqlx::query_scalar("SELECT number from Weeks WHERE is_last_week == 1;") - .fetch_optional(&mut **db) - .await.unwrap_or(Some(0)).unwrap_or(0)) { + // FIXME: Make the 42 not hardcoded + if vote_count < 42 || week == sqlx::query_scalar("SELECT number from Weeks WHERE is_last_week == 1;") + .fetch_optional(&mut **db) + .await.unwrap_or(Some(0)).unwrap_or(0) { return None; } @@ -186,11 +147,12 @@ pub async fn fetch_vote_data(week: u8, mut db: Connection, cookies next_truth_number = raw_vote.truth_number + 1; } - Some(Json(VoteData{truth_count: truth_count, votes: vote_data})) + Some(Json(VoteData{votes: vote_data})) } +// FIXME: pub fn stage() -> AdHoc { - AdHoc::on_ignite("Vote stage", |rocket| async { + AdHoc::on_ignite("SQLx Stage", |rocket| async { rocket.mount("/", routes![vote, fetch_vote_data]) }) } diff --git a/src/week.rs b/src/week.rs index 79608b7..1eb52db 100644 --- a/src/week.rs +++ b/src/week.rs @@ -4,30 +4,17 @@ use rocket::form::Form; use rocket::http::CookieJar; use rocket::response::Redirect; -use rocket_db_pools::{sqlx, sqlx::Row, Connection}; +use rocket_db_pools::{sqlx, Connection}; use rocket_dyn_templates::{context, Template}; use sqlx::{Acquire, Executor}; -use crate::{auth, vote}; +use crate::auth; use crate::auth::User; use crate::database::Db; -use crate::database_records::{DisplayTruth, Player, Tag, Truth, Week}; -use crate::tag; -use crate::vote::WeeklyUserVotes; - -pub async fn get_last_week(db: &mut Connection) -> u8 { - match sqlx::query("SELECT number FROM Weeks WHERE is_last_week == 1;") - .fetch_one(&mut ***db).await { - Ok(row) => row.try_get(0).ok().unwrap_or_else(|| 1), // If error, go back to 1 - Err(error) => { - error!("Error while getting current week : {error:?}"); - 1 - } - } -} +use crate::database_records::{DisplayTruth, Player, Truth, Week}; #[get("/")] pub async fn week(week_number: u8, mut db: Connection, cookies: &CookieJar<'_>) -> Template { - let user: User = auth::get_user(&mut db, cookies).await; + 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") @@ -54,11 +41,9 @@ pub async fn week(week_number: u8, mut db: Connection, cookies: &CookieJar<' } }; - let vote_data: WeeklyUserVotes = vote::get_weekly_user_votes(week_number,&user, &mut db).await; - // FIXME : This is fucking *trash* but fucking hell mate if user.is_admin { - let mut truths: Vec = match sqlx::query_as("SELECT * FROM Truths WHERE week == $1 ORDER BY number") + let truths: Vec = match sqlx::query_as("SELECT * FROM Truths WHERE week == $1 ORDER BY number") .bind(week_number) .fetch_all(&mut **db).await { Ok(v) => v, @@ -68,22 +53,14 @@ pub async fn week(week_number: u8, mut db: Connection, cookies: &CookieJar<' } }; - for truth in &mut truths { - truth.tags = tag::get_truth_tags(truth.id, &mut db).await; - } - - let tags: Vec = tag::get_all_tags(&mut db, cookies).await; - - Template::render("weeks/index", context! { + Template::render("index", context! { week_data: week_data, truths: truths, user: user, other_players: other_players, - vote_data: vote_data, - tags: tags }) } else { - let mut truths: Vec = match sqlx::query_as("SELECT id, week, number, author_id, rendered_text FROM Truths WHERE week == $1 ORDER BY number") + let truths: Vec = 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, @@ -93,16 +70,11 @@ pub async fn week(week_number: u8, mut db: Connection, cookies: &CookieJar<' } }; - for truth in &mut truths { - truth.tags = tag::get_truth_tags(truth.id, &mut db).await; - } - - Template::render("weeks/index", context! { + Template::render("index", context! { week_data: week_data, truths: truths, user: user, other_players: other_players, - vote_data: vote_data }) } } @@ -110,7 +82,7 @@ pub async fn week(week_number: u8, mut db: Connection, cookies: &CookieJar<' #[post("//edit", data="")] pub async fn update_week(week: u8, raw_intro: Form, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; + 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 semaine.")); return Redirect::to(uri!(week(week))); @@ -146,7 +118,7 @@ pub async fn update_week(week: u8, raw_intro: Form, #[post("//set_last")] pub async fn set_last_week(week: u8, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; + 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 semaine.")); return Redirect::to(uri!(week(week))); @@ -201,7 +173,7 @@ pub async fn set_last_week(week: u8, mut db: Connection, cookies: &CookieJar #[get("//create")] pub async fn create_week(week: u8, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { - let user = auth::get_user(&mut db, cookies).await; + 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 semaine.")); return Redirect::to(uri!(week(week - 1))); @@ -238,9 +210,3 @@ pub async fn create_week(week: u8, mut db: Connection, cookies: &CookieJar<' } } } - -pub fn stage() -> AdHoc { - AdHoc::on_ignite("Week stage", |rocket| async { - rocket.mount("/", routes![week, create_week, update_week, set_last_week]) - }) -} diff --git a/static_files/style.css b/static_files/style.css index 0721675..f6cc018 100644 --- a/static_files/style.css +++ b/static_files/style.css @@ -2,22 +2,10 @@ display: flex; flex-direction: row; justify-content: space-between; - align-items: center; margin-right: 2em; } -.top_bar_side { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: baseline; -} - -.top_bar_side > * { - padding: 0 0.5em; -} - -.truth_page_body { +.page_body { display: flex; flex-direction: row; justify-content: space-between; @@ -35,16 +23,6 @@ padding-bottom: 2em; } -.individual_truth h3 { - text-align: center; - padding-bottom: 4px; - border-bottom: slategray solid 0.15em; -} - -.individual_truth p { - padding: 0 0.5em; -} - .editor { width: calc(100% - 1.25em); /* The width is calculated *inside* the padding, so adjust for it. */ height: 6eM; @@ -53,10 +31,6 @@ margin-bottom: 1em; } -.truth_edit_form { - margin-bottom: 1em; -} - .graph { display: flex; flex-direction: column; @@ -73,19 +47,13 @@ top: 25%; } -.week_change:link, .week_change:visited, -.individual_truth > a, .individual_truth > a:link, .individual_truth > a:visited, -nav > a, nav > a:link, nav > a:visited, -h1 > a, h1 > a:link, h1 > a:visited { +.week_change:link, .week_change:visited { color: black; text-decoration: none; transition: all .2s ease-in-out; } -.week_change:hover, .week_change:focus, -.individual_truth > a:hover, .individual_truth > a:focus, -nav > a:hover, nav > a:focus, -h1 > a:hover, h1 > a:focus { +.week_change:hover, .week_change:focus { color: mediumpurple; text-shadow: 0 2px 2px slategray; } @@ -105,99 +73,12 @@ h1 > a:hover, h1 > a:focus { } } -.tag_list { - display: flex; - flex-flow: column wrap; - align-content: flex-start; - max-height: 40vh; - margin-bottom: 1em; -} - -@media (orientation: portrait) { - .tag_list { - max-height: none; - } -} - -.new_tag { - display: flex; - flex-direction: column; - border: black dashed 1px; - padding: 0.5em; - margin-top: 0.5em; - width: fit-content; -} - -.new_tag > button { - margin-top: 1em; -} - -.tag { - display: flex; - justify-content: center; - margin: 0.25em; - width: fit-content; - text-decoration: none; - text-shadow: - -1px 0 3px black, - 1px 0 3px black, - 0 -1px 3px black, - 0 1px 3px black; - color: white; - padding: 0.25em 0.75em 0.25em 0.25em; - border-bottom-left-radius: 0.4em; - border-top-left-radius: 0.4em; - border-bottom-right-radius: 1.5em 0.8em; - border-top-right-radius: 1.5em 0.8em; - - transition: all .2s ease-in-out; -} - -.tag:hover, .tag:focus { - color: mediumpurple; -} - -.truth_tags { - display: flex; - flex-flow: wrap; -} - -.truth_tags > .tag { /* We want smaller tags, the shadows must follow as well */ - font-size: smaller; - text-shadow: - -1px 0 2px black, - 1px 0 2px black, - 0 -1px 2px black, - 0 1px 2px black; -} - -.truth_tags > form { - display: contents; -} - -.login { - display: flex; - flex-direction: column; -} - -.login > * { - padding-bottom: 0.1em; -} - -.login > label { - align-self: flex-end; -} - -.login > b { - align-self: center; -} - /* Global styling */ hr { border: none; - border-top: 2px dotted slategray; - color: slategray; + border-top: 2px solid #9ec5fe; + color: #9ec5fe; overflow: visible; text-align: center; height: 5px; @@ -221,10 +102,3 @@ body { body > h2 { padding-left: 0.25eM; } - -nav { - height: fit-content; - margin: 0 0.25em; - border-left: 0.15em solid slategray; - border-right: 0.15em solid slategray; -} diff --git a/static_files/vote_chart.js b/static_files/vote_chart.js index 306a01d..09850d4 100644 --- a/static_files/vote_chart.js +++ b/static_files/vote_chart.js @@ -1,27 +1,23 @@ const limit_ratio = 1.3 const colors = ['#b6b8fc', '#b6f4fc', '#fcb6cc', '#e0fcb6', '#fcdcb6', '#b6fcc8', '#f0b6fc'] async function main() { - let current_url = new URL(document.URL) - current_url.hash = '' // Strip the local part of the URL, for example if we select a Truth - const vote_response = await fetch(current_url.toString()+"/votes"); + const vote_response = await fetch(document.URL+"/votes"); if (!vote_response.ok) { return; } - let keys = [] + const keys = ["Vérité 1", "Vérité 2", "Vérité 3", "Vérité 4", "Vérité 5", "Vérité 6", "Vérité 7"] let datasets = [] + try { - const vote_data = (await vote_response.json()); - for (let player in vote_data.votes) { + const vote_data = (await vote_response.json()).votes; + for (let player in vote_data) { datasets.push({ parsing: true, label: player, - data: vote_data.votes[player], + data: vote_data[player], }) } - for (let i = 1; i <= vote_data.truth_count; i++) { - keys.push("Vérité " + i) - } } catch (error) { console.error("Failed to parse vote data : \n\t" + error.message); return; diff --git a/templates/base_page.html.tera b/templates/base_page.html.tera deleted file mode 100644 index c598fa8..0000000 --- a/templates/base_page.html.tera +++ /dev/null @@ -1,50 +0,0 @@ - - -{% set title = "Vérités Nova Borealis" %} - - - {{ title }} - - - {% block scripts %} - {% endblock %} - - - -{% block top_bar %} -
-
-

{{ title }}

- -
-
- {% block top_bar_side %} -
- {% if user.logged_in == true %} - - Connecté en tant que {{ user.name }} - - - {% else %} - - {% endif %} -
- {% endblock %} -
-
-{% endblock %} - -{% block body %} -{% endblock %} - - - diff --git a/templates/editable_truth.tera b/templates/editable_truth.tera new file mode 100644 index 0000000..22d3470 --- /dev/null +++ b/templates/editable_truth.tera @@ -0,0 +1,8 @@ +
+

Vérité {{ truth.number }}

+

{{ truth.rendered_text | safe }}

+
+
+ {% include "truth_editor" %} +
+
diff --git a/templates/index.html.tera b/templates/index.html.tera new file mode 100644 index 0000000..c2f3afa --- /dev/null +++ b/templates/index.html.tera @@ -0,0 +1,135 @@ + + +{% set title = "Vérités Fabula Ultima" %} + + + {{ title }} + + {% if user.logged_in == true and not user.is_admin %} + + {% endif %} + + + + +{#{% import "week_change_arrows" as week_macro %}#} +{# For some reason the import does not work ? Figure it out at some point... #} +{%- macro display(display_character, to, enabled) -%} + {%- set class = "week_change" -%} + {%- if enabled == true %} + {% set target = ("href=/" ~ to) %} + {%- else -%} + {% set class = class ~ " week_change_hidden" -%} + {% set target = "" %} + {%- endif -%} + {%- if enabled == true -%}{{- display_character -}}{%- endif -%} +{%- endmacro display -%} + + +{% set back_arrow_enabled = week_data.number > 1 %} +{% set next_arrow_enabled = (week_data.is_last_week != true or user.is_admin == true) %} +{% set next_arrow_href = (week_data.number + 1) %} +{% if user.is_admin == true %} + {% set next_arrow_href = next_arrow_href ~ "/create" %} + {% set next_arrow_chara = '⥅' %} +{% else %} + {% set next_arrow_chara = '⟹' %} +{% endif %} + + +
+

{{ title }}

+ {% if user.logged_in == true %} +

Connecté en tant que {{ user.name }}

+ {% else %} + + {% endif %} +
+ +

{{- self::display(display_character='⟸', to=(week_data.number - 1), enabled=back_arrow_enabled) }} + Semaine {{ week_data.number }} + {{- self::display(display_character=next_arrow_chara, to=next_arrow_href, enabled=next_arrow_enabled) -}}

+
+
+ {% if user.is_admin == true and week_data.is_last_week != true %} +
+ +
+ {% endif %} +
+ {{ week_data.rendered_text | safe }} + {%- if user.is_admin == true -%} +
+
+ + +
+ {% endif %} +
+ {% if user.logged_in == true and user.is_admin == false %} +
+ {% endif %} + + {# Truths start at 1 but the array starts at 0 #} + {% set index_delta = 1 %} + {% for truth in truths %} + {# + The truths are in an ordered array, but one of them might be the user's. + In this case, we need to stop the array index from incrementing if the current + truth is the user's, as they cannot have voted for themselves, leading to one + less votes than there are truths. + #} + {%- if truth.author_id == user.id -%} + {%- set_global index_delta = 2 -%} + {% endif %} + {% set truth_index = truth.number - index_delta %} + + {% if user.is_admin == true %} + {% include "editable_truth" %} + {% else %} + {% include "truth" %} + {% endif %} + {% endfor %} + + {% if user.logged_in == true and user.is_admin == false %} +
+ +
+ {% endif %} + + {# If admin, show an additional box for creating a new Truth. #} + {% if user.is_admin == true %} +
+

Nouvelle vérité

+
+ {% include "truth_editor" %} +
+
+ {% endif %} +
+ +
+
+ +
+
+
+ + + diff --git a/templates/tags/filtered_truths.html.tera b/templates/tags/filtered_truths.html.tera deleted file mode 100644 index cb94d54..0000000 --- a/templates/tags/filtered_truths.html.tera +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "base_page" %} - -{% block top_bar_side %} -{% endblock %} - -{% block body %} - {% if tags | length == 0 %} -

Ensemble des vérités à ce jour

- {% elif tags | length == 1 %} -

Vérités correponsdantes au thème

- {% else %} -

Vérités correponsdantes aux thèmes

- {% endif %} - -
- {% for tag in tags %} - {{ tag.name }} - {% endfor %} -
- - {% for truth in truths %} -
-

Semaine {{ truth.week }} - Vérité {{ truth.number }}

-

{{ truth.rendered_text | safe }}

-
- {% endfor %} -{% endblock %} diff --git a/templates/tags/index.html.tera b/templates/tags/index.html.tera deleted file mode 100644 index 5e423d4..0000000 --- a/templates/tags/index.html.tera +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "base_page" %} - -{% block top_bar_side %} -{% endblock %} - -{% block body %} -

Liste des thèmes de Vérité

- -
- {% for tag in tags %} -
- -
- {% endfor %} -
- - - {% if user.is_admin %} -
- -

Éditer les thèmes

- -
- {% for tag in tags %} -
- - - - -
- {% endfor %} -
- -
-
- - -
- -
- {% endif %} -{% endblock %} diff --git a/templates/weeks/truth.html.tera b/templates/truth.html.tera similarity index 69% rename from templates/weeks/truth.html.tera rename to templates/truth.html.tera index 5eaa580..14df3e5 100644 --- a/templates/weeks/truth.html.tera +++ b/templates/truth.html.tera @@ -1,18 +1,12 @@ {%- set is_disabled = "" -%} {# If we are not during the active week, prevent changing vote but not sending a missing one. #} -{%- if week_data.is_last_week != true and vote_data.votes | filter(attribute="truth_id", value=truth.id) -%} +{%- if week_data.is_last_week != true and user.votes | filter(attribute="truth_id", value=truth.id) -%} {%- set is_disabled = "disabled" -%} {%- endif -%}
-

Vérité {{ truth.number }}

+

Vérité {{ truth.number }}

{{ truth.rendered_text | safe }}

-
-
- {% for tag in truth.tags %} - {{ tag.name }} - {% endfor %} -
{% if user.logged_in %}