diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 2c18987..5e560b7 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -21,7 +21,9 @@ jobs: include: - { os: "macos-latest", target: "aarch64-apple-darwin" } - { os: "ubuntu-latest", target: "x86_64-unknown-linux-gnu" } + - { os: "ubuntu-latest", target: "x86_64-unknown-linux-musl" } - { os: "ubuntu-24.04-arm", target: "aarch64-unknown-linux-gnu" } + - { os: "ubuntu-24.04-arm", target: "aarch64-unknown-linux-musl" } - { os: "windows-latest", target: "x86_64-pc-windows-msvc" } runs-on: ${{ matrix.os }} @@ -40,7 +42,7 @@ jobs: if: contains(matrix.os, 'ubuntu') uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev lld + packages: libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev lld musl-tools # don't wanna branch for musl version: latest # could be anything - name: install rust target diff --git a/.gitignore b/.gitignore index c28343c..83f5d9e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ **/Cargo.lock assets/tailwind.css -.**-apollo-sid.cookie +.*-apollo-sid.cookie diff --git a/AGENTS.md b/AGENTS.md index ef4ddde..86ca4b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,3 +18,9 @@ The role of this file is to describe common mistakes and confusion points that a **NOTE**: only read this if the code change will effect core parts with `dioxus` you can find in-depth `dioxus` docs under <./.docs/dioxus.md> + +--- + +## surprises encountered + +- `make check` also runs `cargo check -F web --target wasm32-unknown-unknown`; in fresh environments the `wasm32-unknown-unknown` target may be missing, so run `rustup target add wasm32-unknown-unknown` first if needed. diff --git a/Makefile b/Makefile index d5ab0a8..b714709 100644 --- a/Makefile +++ b/Makefile @@ -76,7 +76,8 @@ server-build: cp target/x86_64-unknown-linux-gnu/release/apollo apollo-server-x64-linux-gnu clean: - rm .user-*-apollo-sid.cookie | echo 'no sid-cookies to delete' + rm .*-apollo-sid.cookie || echo 'no sid-cookies to delete' + rm apollo-state.cbor.encrypted || echo 'no default state-save to delete' cargo clean help list: diff --git a/src/backend/endpoints.rs b/src/backend/endpoints.rs index 6e816b9..aa75fb5 100644 --- a/src/backend/endpoints.rs +++ b/src/backend/endpoints.rs @@ -68,7 +68,7 @@ pub async fn join(username: String) -> Result, HttpError> { _ = TEAMS .write() .await - .insert(username.clone(), SolvedPuzzles::new()); + .insert(username.clone(), TeamAttempts::new()); } // allowed to log in, but don't reset progress @@ -220,21 +220,26 @@ pub async fn submit_solution( .or_internal_server_error("nem sikerült ellenőrizni a feladatmegoldást")? }; solution.zeroize(); - is_solution_valid.or_forbidden("érvénytelen megoldás ehhez a feladathoz")?; + let is_correct = is_solution_valid; let mut teams_lock = TEAMS.write().await; - let team_solved = teams_lock + let team_attempts = teams_lock .get_mut(&username) .or_internal_server_error("nincs ehhez a csapatnévhez előrehaladás rendelve")?; - team_solved - .insert(puzzle_id) + (!team_has_solved_puzzle(team_attempts, &puzzle_id)) .or_forbidden("ezt a feladatot már megoldottad")?; + + team_attempts.push(SolveAttempt::now(puzzle_id, is_correct)); drop(teams_lock); #[cfg(feature = "server_state_save")] tokio::spawn(state_save::save_state()); - Ok(String::from("hurrá, sikeresen elmentettük a megoldásod!")) + if is_correct { + Ok(String::from("hurrá, sikeresen elmentettük a megoldásod!")) + } else { + HttpError::forbidden("érvénytelen megoldás ehhez a feladathoz") + } } diff --git a/src/backend/models.rs b/src/backend/models.rs index d02b96a..48c0a3f 100644 --- a/src/backend/models.rs +++ b/src/backend/models.rs @@ -1,5 +1,6 @@ use dioxus::fullstack::serde; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; +use std::time::SystemTime; // SECURITY: SecretString, with manual impls? #[derive(Clone, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize)] @@ -20,7 +21,56 @@ pub type PuzzleSolutionHash = String; pub type PuzzlesExisting = HashMap; /// all the puzzles with their values and solutions pub type PuzzleSolutions = HashMap; -/// solved puzzles of a team -pub type SolvedPuzzles = HashSet; -/// progress of each team, which puzzles they've solved -pub type TeamsState = HashMap; + +#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(crate = "dioxus::fullstack::serde")] +pub struct SolveAttempt { + pub puzzle_id: PuzzleId, + pub attempted_at: SystemTime, + pub correct: bool, +} +impl SolveAttempt { + pub fn now(puzzle_id: PuzzleId, correct: bool) -> Self { + Self { + puzzle_id, + attempted_at: SystemTime::now(), + correct, + } + } +} + +/// all solve attempts of a team +pub type TeamAttempts = Vec; +/// progress of each team, which puzzles they attempted and when +pub type TeamsState = HashMap; + +pub fn team_has_solved_puzzle(team_attempts: &[SolveAttempt], puzzle_id: &str) -> bool { + team_attempts + .iter() + .any(|attempt| attempt.puzzle_id == puzzle_id && attempt.correct) +} + +#[cfg(test)] +mod tests { + use super::{SolveAttempt, team_has_solved_puzzle}; + use std::time::UNIX_EPOCH; + + #[test] + fn marks_puzzle_as_solved_only_for_correct_attempts() { + let team_attempts = vec![ + SolveAttempt { + puzzle_id: "p1".into(), + attempted_at: UNIX_EPOCH, + correct: false, + }, + SolveAttempt { + puzzle_id: "p1".into(), + attempted_at: UNIX_EPOCH, + correct: true, + }, + ]; + + assert!(team_has_solved_puzzle(&team_attempts, "p1")); + assert!(!team_has_solved_puzzle(&team_attempts, "p2")); + } +}