add `/<uuid> route + backend handling

This commit is contained in:
2025-08-02 19:11:39 +02:00
parent 9badb4a4ad
commit 96a9ab361a
10 changed files with 116 additions and 50 deletions

View File

@@ -1,26 +1,26 @@
-- Enable foreign key constraints -- Enable foreign key constraints
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
CREATE TABLE user ( CREATE TABLE client (
uuid TEXT PRIMARY KEY, uuid TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL name TEXT
); );
CREATE TABLE camera ( CREATE TABLE camera (
uuid TEXT PRIMARY KEY, uuid TEXT PRIMARY KEY NOT NULL,
desc TEXT, desc TEXT,
name TEXT NOT NULL name TEXT NOT NULL
); );
CREATE TABLE sightings ( CREATE TABLE sightings (
user_uuid TEXT NOT NULL, client_uuid TEXT NOT NULL,
sighted_at DATETIME NOT NULL, sighted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
camera_id TEXT NOT NULL, 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, 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 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); CREATE INDEX idx_sightings_camera_id ON sightings(camera_id);

View File

@@ -1,5 +1,5 @@
-- Insert test users -- Insert test clients
INSERT INTO user (uuid, name) VALUES INSERT INTO client (uuid, name) VALUES
('550e8400-e29b-41d4-a716-446655440001', 'Alice Johnson'), ('550e8400-e29b-41d4-a716-446655440001', 'Alice Johnson'),
('550e8400-e29b-41d4-a716-446655440002', 'Bob Smith'), ('550e8400-e29b-41d4-a716-446655440002', 'Bob Smith'),
('550e8400-e29b-41d4-a716-446655440003', 'Carol Williams'), ('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'); ('750e8400-e29b-41d4-a716-446655440006', 'Rooftop panoramic view', 'Rooftop Cam');
-- Insert test sightings -- 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 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-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'), ('550e8400-e29b-41d4-a716-446655440002', '2025-08-01 11:08:12', '750e8400-e29b-41d4-a716-446655440003'),

View File

@@ -1,2 +0,0 @@
use sqlx::SqlitePool;

View File

@@ -1,7 +1,7 @@
use crate::{client_id, page::new, Backend}; use crate::{page::new, Backend};
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{IntoResponse, Response}, response::{IntoResponse, Redirect, Response},
routing::get, routing::get,
Router, Router,
}; };
@@ -11,9 +11,9 @@ use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
async fn index(State(backend): State<Arc<Backend>>, cookies: CookieJar) -> Response { async fn index(State(backend): State<Arc<Backend>>, 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 amount_total_cameras = backend.amount_total_cameras().await;
let markup = new(html! { let markup = new(html! {
@@ -44,25 +44,25 @@ async fn index(State(backend): State<Arc<Backend>>, cookies: CookieJar) -> Respo
(cookies, markup).into_response() (cookies, markup).into_response()
} }
async fn game(cookies: CookieJar, Path(uuid): Path<String>) -> Response { async fn game(
let (cookies, client) = client_id(cookies); State(backend): State<Arc<Backend>>,
cookies: CookieJar,
Path(uuid): Path<String>,
) -> Result<Redirect, Response> {
let (cookies, client) = backend.client(cookies).await;
let Ok(uuid) = Uuid::parse_str(&uuid) else { 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! { let Some(camera) = backend.camera_by_uuid(uuid).await else {
hgroup { return Err(not_found().await.into_response());
h1 { "Digital Shadows" (PreEscaped("&mdash;")) "Who finds the most cameras?" } };
h2 {
(client)
" found camera "
(uuid)
}
}
});
(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 { async fn not_found() -> Markup {

View File

@@ -1,3 +1,4 @@
use crate::model::client::Client;
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use axum_extra::extract::{cookie::Cookie, CookieJar}; use axum_extra::extract::{cookie::Cookie, CookieJar};
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, SqlitePool}; use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, SqlitePool};
@@ -14,19 +15,21 @@ pub(crate) enum Backend {
Sqlite(SqlitePool), Sqlite(SqlitePool),
} }
fn client_id(cookies: CookieJar) -> (CookieJar, String) { impl Backend {
let mut cookies = cookies; async fn client(&self, cookies: CookieJar) -> (CookieJar, Client) {
if cookies.get("client_id").is_none() { let existing_uuid = cookies
let id = Uuid::new_v4().to_string(); .get("client_id")
cookies = cookies.add(Cookie::new("client_id", 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] #[tokio::main]

View File

@@ -1,15 +1,30 @@
use crate::Backend; use crate::Backend;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::FromRow; use sqlx::FromRow;
use uuid::Uuid;
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Camera { pub struct Camera {
pub uuid: String, pub uuid: String,
pub desc: String, pub desc: Option<String>,
pub name: String, pub name: String,
} }
impl Backend { impl Backend {
pub(crate) async fn camera_by_uuid(&self, uuid: Uuid) -> Option<Camera> {
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 { pub(crate) async fn amount_total_cameras(&self) -> i64 {
match self { match self {
Backend::Sqlite(db) => { Backend::Sqlite(db) => {

29
src/model/client.rs Normal file
View File

@@ -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<String>,
}
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")
}
}
}
}

View File

@@ -1,3 +1,3 @@
pub(crate) mod camera; pub(crate) mod camera;
pub(crate) mod client;
pub(crate) mod sighting; pub(crate) mod sighting;
pub(crate) mod user;

View File

@@ -1,20 +1,24 @@
use crate::Backend; use crate::{
model::{camera::Camera, client::Client},
Backend,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{types::chrono::NaiveDateTime, FromRow}; use sqlx::{types::chrono::NaiveDateTime, FromRow};
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Sighting { pub struct Sighting {
pub user_uuid: String, pub client_uuid: String,
pub sighted_at: NaiveDateTime, pub sighted_at: NaiveDateTime,
pub camera_id: String, // Changed from i64 to String to match TEXT/UUID in schema pub camera_id: String, // Changed from i64 to String to match TEXT/UUID in schema
} }
impl Backend { impl Backend {
pub(crate) async fn sightings_for_user_uuid(&self, uuid: &str) -> Vec<Sighting> { pub(crate) async fn sightings_for_client(&self, client: &Client) -> Vec<Sighting> {
let uuid = client.uuid.to_string();
match self { match self {
Backend::Sqlite(db) => sqlx::query_as!( Backend::Sqlite(db) => sqlx::query_as!(
Sighting, 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 uuid
) )
.fetch_all(db) .fetch_all(db)
@@ -22,4 +26,21 @@ impl Backend {
.unwrap(), .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
}
} }

View File