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