diff --git a/migration.sql b/migration.sql index de4cd79..4d582db 100644 --- a/migration.sql +++ b/migration.sql @@ -1,26 +1,26 @@ -- Enable foreign key constraints PRAGMA foreign_keys = ON; -CREATE TABLE user ( - uuid TEXT PRIMARY KEY, - name TEXT NOT NULL +CREATE TABLE client ( + uuid TEXT PRIMARY KEY NOT NULL, + name TEXT ); CREATE TABLE camera ( - uuid TEXT PRIMARY KEY, + uuid TEXT PRIMARY KEY NOT NULL, desc TEXT, name TEXT NOT NULL ); CREATE TABLE sightings ( - user_uuid TEXT NOT NULL, - sighted_at DATETIME NOT NULL, + client_uuid TEXT NOT NULL, + sighted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, camera_id TEXT NOT NULL, - FOREIGN KEY (user_uuid) REFERENCES user(uuid) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (client_uuid) REFERENCES client(uuid) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (camera_id) REFERENCES camera(uuid) ON DELETE CASCADE ON UPDATE CASCADE, - UNIQUE (user_uuid, camera_id) + UNIQUE (client_uuid, camera_id) ); -- Create indexes for better performance on foreign key lookups -CREATE INDEX idx_sightings_user_uuid ON sightings(user_uuid); +CREATE INDEX idx_sightings_client_uuid ON sightings(client_uuid); CREATE INDEX idx_sightings_camera_id ON sightings(camera_id); diff --git a/seeds_test.sql b/seeds_test.sql index 2f881f9..cf9ac40 100644 --- a/seeds_test.sql +++ b/seeds_test.sql @@ -1,5 +1,5 @@ --- Insert test users -INSERT INTO user (uuid, name) VALUES +-- Insert test clients +INSERT INTO client (uuid, name) VALUES ('550e8400-e29b-41d4-a716-446655440001', 'Alice Johnson'), ('550e8400-e29b-41d4-a716-446655440002', 'Bob Smith'), ('550e8400-e29b-41d4-a716-446655440003', 'Carol Williams'), @@ -16,7 +16,7 @@ INSERT INTO camera (uuid, desc, name) VALUES ('750e8400-e29b-41d4-a716-446655440006', 'Rooftop panoramic view', 'Rooftop Cam'); -- Insert test sightings -INSERT INTO sightings (user_uuid, sighted_at, camera_id) VALUES +INSERT INTO sightings (client_uuid, sighted_at, camera_id) VALUES ('550e8400-e29b-41d4-a716-446655440001', '2025-08-01 09:15:30', '750e8400-e29b-41d4-a716-446655440001'), ('550e8400-e29b-41d4-a716-446655440001', '2025-08-01 14:22:45', '750e8400-e29b-41d4-a716-446655440002'), ('550e8400-e29b-41d4-a716-446655440002', '2025-08-01 11:08:12', '750e8400-e29b-41d4-a716-446655440003'), diff --git a/src/backend.rs b/src/backend.rs deleted file mode 100644 index e87fdfa..0000000 --- a/src/backend.rs +++ /dev/null @@ -1,2 +0,0 @@ -use sqlx::SqlitePool; - diff --git a/src/game.rs b/src/game.rs index 7ae57cc..4a35018 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,7 +1,7 @@ -use crate::{client_id, page::new, Backend}; +use crate::{page::new, Backend}; use axum::{ extract::{Path, State}, - response::{IntoResponse, Response}, + response::{IntoResponse, Redirect, Response}, routing::get, Router, }; @@ -11,9 +11,9 @@ use std::sync::Arc; use uuid::Uuid; async fn index(State(backend): State>, cookies: CookieJar) -> Response { - let (cookies, client) = client_id(cookies); + let (cookies, client) = backend.client(cookies).await; - let sightings = backend.sightings_for_user_uuid(&client).await; + let sightings = backend.sightings_for_client(&client).await; let amount_total_cameras = backend.amount_total_cameras().await; let markup = new(html! { @@ -44,25 +44,25 @@ async fn index(State(backend): State>, cookies: CookieJar) -> Respo (cookies, markup).into_response() } -async fn game(cookies: CookieJar, Path(uuid): Path) -> Response { - let (cookies, client) = client_id(cookies); +async fn game( + State(backend): State>, + cookies: CookieJar, + Path(uuid): Path, +) -> Result { + let (cookies, client) = backend.client(cookies).await; let Ok(uuid) = Uuid::parse_str(&uuid) else { - return not_found().await.into_response(); + return Err(not_found().await.into_response()); }; - let markup = new(html! { - hgroup { - h1 { "Digital Shadows" (PreEscaped("—")) "Who finds the most cameras?" } - h2 { - (client) - " found camera " - (uuid) - } - } - }); + let Some(camera) = backend.camera_by_uuid(uuid).await else { + return Err(not_found().await.into_response()); + }; - (cookies, markup).into_response() + let succ = backend.client_found_camera(&client, &camera).await; + // TODO: show succ/err based on succ + + Ok(Redirect::to("/game")) } async fn not_found() -> Markup { diff --git a/src/main.rs b/src/main.rs index 9c6b86d..2854467 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use crate::model::client::Client; use axum::{routing::get, Router}; use axum_extra::extract::{cookie::Cookie, CookieJar}; use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, SqlitePool}; @@ -14,19 +15,21 @@ pub(crate) enum Backend { Sqlite(SqlitePool), } -fn client_id(cookies: CookieJar) -> (CookieJar, String) { - let mut cookies = cookies; - if cookies.get("client_id").is_none() { - let id = Uuid::new_v4().to_string(); - cookies = cookies.add(Cookie::new("client_id", id)) +impl Backend { + async fn client(&self, cookies: CookieJar) -> (CookieJar, Client) { + let existing_uuid = cookies + .get("client_id") + .and_then(|cookie| Uuid::parse_str(cookie.value()).ok()); + + match existing_uuid { + Some(uuid) => (cookies, self.get_client(&uuid).await), + None => { + let new_id = Uuid::new_v4(); + let updated_cookies = cookies.add(Cookie::new("client_id", new_id.to_string())); + (updated_cookies, self.get_client(&new_id).await) + } + } } - - let id = cookies - .get("client_id") - .expect("can't happen, as we checked above") - .to_string(); - - (cookies, id) } #[tokio::main] diff --git a/src/model/camera.rs b/src/model/camera.rs index d352a73..1e51804 100644 --- a/src/model/camera.rs +++ b/src/model/camera.rs @@ -1,15 +1,30 @@ use crate::Backend; use serde::{Deserialize, Serialize}; use sqlx::FromRow; +use uuid::Uuid; #[derive(FromRow, Debug, Serialize, Deserialize)] pub struct Camera { pub uuid: String, - pub desc: String, + pub desc: Option, pub name: String, } impl Backend { + pub(crate) async fn camera_by_uuid(&self, uuid: Uuid) -> Option { + let uuid = uuid.to_string(); + match self { + Backend::Sqlite(db) => sqlx::query_as!( + Camera, + "SELECT uuid, desc, name FROM camera WHERE uuid = ?", + uuid + ) + .fetch_optional(db) + .await + .unwrap(), + } + } + pub(crate) async fn amount_total_cameras(&self) -> i64 { match self { Backend::Sqlite(db) => { diff --git a/src/model/client.rs b/src/model/client.rs new file mode 100644 index 0000000..f5f5e9d --- /dev/null +++ b/src/model/client.rs @@ -0,0 +1,29 @@ +use crate::Backend; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct Client { + pub uuid: String, + pub name: Option, +} + +impl Backend { + pub(crate) async fn get_client(&self, uuid: &Uuid) -> Client { + let uuid = uuid.to_string(); + + match self { + Backend::Sqlite(db) => { + sqlx::query!("INSERT OR IGNORE INTO client (uuid) VALUES (?);", uuid) + .execute(db) + .await + .unwrap(); + sqlx::query_as!(Client, "SELECT uuid, name FROM client WHERE uuid = ?", uuid) + .fetch_one(db) + .await + .expect("we assured that uuid exists in previous query") + } + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 9890072..399109a 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,3 +1,3 @@ pub(crate) mod camera; +pub(crate) mod client; pub(crate) mod sighting; -pub(crate) mod user; diff --git a/src/model/sighting.rs b/src/model/sighting.rs index 3125d10..5e7e05a 100644 --- a/src/model/sighting.rs +++ b/src/model/sighting.rs @@ -1,20 +1,24 @@ -use crate::Backend; +use crate::{ + model::{camera::Camera, client::Client}, + Backend, +}; use serde::{Deserialize, Serialize}; use sqlx::{types::chrono::NaiveDateTime, FromRow}; #[derive(FromRow, Debug, Serialize, Deserialize)] pub struct Sighting { - pub user_uuid: String, + pub client_uuid: String, pub sighted_at: NaiveDateTime, pub camera_id: String, // Changed from i64 to String to match TEXT/UUID in schema } impl Backend { - pub(crate) async fn sightings_for_user_uuid(&self, uuid: &str) -> Vec { + pub(crate) async fn sightings_for_client(&self, client: &Client) -> Vec { + let uuid = client.uuid.to_string(); match self { Backend::Sqlite(db) => sqlx::query_as!( Sighting, - "SELECT user_uuid, sighted_at, camera_id FROM sightings WHERE user_uuid = ?", + "SELECT client_uuid, sighted_at, camera_id FROM sightings WHERE client_uuid = ?", uuid ) .fetch_all(db) @@ -22,4 +26,21 @@ impl Backend { .unwrap(), } } + + pub(crate) async fn client_found_camera(&self, client: &Client, camera: &Camera) -> bool { + let client_uuid = client.uuid.to_string(); + let camera_uuid = camera.uuid.to_string(); + + match self { + Backend::Sqlite(db) => sqlx::query!( + "INSERT INTO sightings(client_uuid, camera_id) VALUES (?, ?) RETURNING client_uuid", + client_uuid, + camera_uuid + ) + .fetch_one(db) + .await + .unwrap(), + }; + true + } } diff --git a/src/model/user.rs b/src/model/user.rs deleted file mode 100644 index e69de29..0000000