add `/<uuid> route + backend handling
This commit is contained in:
@@ -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);
|
||||
|
@@ -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'),
|
||||
|
@@ -1,2 +0,0 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
36
src/game.rs
36
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<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("—")) "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 {
|
||||
|
27
src/main.rs
27
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]
|
||||
|
@@ -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
29
src/model/client.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,3 +1,3 @@
|
||||
pub(crate) mod camera;
|
||||
pub(crate) mod client;
|
||||
pub(crate) mod sighting;
|
||||
pub(crate) mod user;
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user