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
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);

View File

@@ -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'),

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::{
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<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 markup = new(html! {
@@ -44,25 +44,25 @@ async fn index(State(backend): State<Arc<Backend>>, cookies: CookieJar) -> Respo
(cookies, markup).into_response()
}
async fn game(cookies: CookieJar, Path(uuid): Path<String>) -> Response {
let (cookies, client) = client_id(cookies);
async fn game(
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 {
return not_found().await.into_response();
return Err(not_found().await.into_response());
};
let markup = new(html! {
hgroup {
h1 { "Digital Shadows" (PreEscaped("&mdash;")) "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 {

View File

@@ -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]

View File

@@ -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<String>,
pub name: String,
}
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 {
match self {
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 client;
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 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<Sighting> {
pub(crate) async fn sightings_for_client(&self, client: &Client) -> Vec<Sighting> {
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
}
}

View File