add sqlite db; add todos :-(; show current cameras

This commit is contained in:
2025-08-02 17:46:56 +02:00
parent 3f61f675e9
commit fe89c86004
14 changed files with 1653 additions and 31 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL=sqlite://db.sqlite

1522
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,10 @@ edition = "2024"
[dependencies]
axum = "0.8"
axum-extra = { version = "0.10", features = ["cookie"] }
maud = { version = "0.27.0", features = ["axum"] }
tokio = { version = "1.47.0", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.6.6", features = ["fs"] }
uuid = { version = "1.17.0", features = ["v4", "serde"] }
chrono = { version = "0.4.41", features = ["serde"] }
maud = { version = "0.27", features = ["axum"] }
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "chrono"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.6", features = ["fs"] }
uuid = { version = "1.17", features = ["v4", "serde"] }

BIN
db.sqlite Normal file

Binary file not shown.

26
migration.sql Normal file
View File

@@ -0,0 +1,26 @@
-- Enable foreign key constraints
PRAGMA foreign_keys = ON;
CREATE TABLE user (
uuid TEXT PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE camera (
uuid TEXT PRIMARY KEY,
desc TEXT,
name TEXT NOT NULL
);
CREATE TABLE sightings (
user_uuid TEXT NOT NULL,
sighted_at DATETIME NOT NULL,
camera_id TEXT NOT NULL,
FOREIGN KEY (user_uuid) REFERENCES user(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)
);
-- Create indexes for better performance on foreign key lookups
CREATE INDEX idx_sightings_user_uuid ON sightings(user_uuid);
CREATE INDEX idx_sightings_camera_id ON sightings(camera_id);

2
src/backend.rs Normal file
View File

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

View File

@@ -1,29 +1,51 @@
use crate::{client_id, page::new};
use crate::{client_id, page::new, Backend};
use axum::{
extract::Path,
extract::{Path, State},
response::{IntoResponse, Response},
routing::get,
Router,
};
use axum_extra::extract::CookieJar;
use maud::{html, Markup, PreEscaped};
use std::sync::Arc;
use uuid::Uuid;
fn page(content: Markup) -> Markup {
new(html! {
async fn index(State(backend): State<Arc<Backend>>, cookies: CookieJar) -> Response {
let (cookies, client) = client_id(cookies);
let sightings = backend.sightings_for_user_uuid(&client).await;
let amount_total_cameras = backend.amount_total_cameras().await;
let markup = new(html! {
hgroup {
h1 { "Digital Shadows" (PreEscaped("&mdash;")) "Who finds the most cameras?" }
(content)
}
})
p {
mark { "TODO: Explanation of AEF / digital shadows / search game" }
}
p {
mark { "TODO: Show optional SUCC message" }
}
p {
mark { "TODO: Show optional REGISTER-NAME message" }
}
p {
"You have found "
(sightings.len())
"/"
(amount_total_cameras)
" cameras."
}
p {
mark { "TODO: High score" }
}
});
async fn index() -> Markup {
page(html! {})
(cookies, markup).into_response()
}
async fn game(cookies: CookieJar, Path(uuid): Path<String>) -> Response {
let (cookies, device) = client_id(cookies);
let (cookies, client) = client_id(cookies);
let Ok(uuid) = Uuid::parse_str(&uuid) else {
return not_found().await.into_response();
@@ -33,7 +55,7 @@ async fn game(cookies: CookieJar, Path(uuid): Path<String>) -> Response {
hgroup {
h1 { "Digital Shadows" (PreEscaped("&mdash;")) "Who finds the most cameras?" }
h2 {
(device)
(client)
" found camera "
(uuid)
}
@@ -49,7 +71,7 @@ async fn not_found() -> Markup {
})
}
pub(super) fn routes() -> Router {
pub(super) fn routes() -> Router<Arc<Backend>> {
Router::new()
.route("/game", get(index))
.route("/{*uuid}", get(game))

View File

@@ -1,12 +1,19 @@
use axum::{routing::get, Router};
use axum_extra::extract::{cookie::Cookie, CookieJar};
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, SqlitePool};
use std::{str::FromStr, sync::Arc};
use tower_http::services::ServeDir;
use uuid::Uuid;
mod game;
mod index;
pub(crate) mod model;
mod page;
pub(crate) enum Backend {
Sqlite(SqlitePool),
}
fn client_id(cookies: CookieJar) -> (CookieJar, String) {
let mut cookies = cookies;
if cookies.get("client_id").is_none() {
@@ -24,10 +31,17 @@ fn client_id(cookies: CookieJar) -> (CookieJar, String) {
#[tokio::main]
async fn main() {
let connection_options = SqliteConnectOptions::from_str("sqlite://db.sqlite").unwrap();
let db: SqlitePool = PoolOptions::new()
.connect_with(connection_options)
.await
.unwrap();
let app = Router::new()
.route("/", get(index::index))
.nest_service("/static", ServeDir::new("./static/serve"))
.merge(game::routes());
.merge(game::routes())
.with_state(Arc::new(Backend::Sqlite(db)));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();

24
src/model/camera.rs Normal file
View File

@@ -0,0 +1,24 @@
use crate::Backend;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Camera {
pub uuid: String,
pub desc: String,
pub name: String,
}
impl Backend {
pub(crate) async fn amount_total_cameras(&self) -> i64 {
match self {
Backend::Sqlite(db) => {
sqlx::query!("SELECT COUNT(*) as count FROM camera")
.fetch_one(db)
.await
.unwrap()
.count
}
}
}
}

3
src/model/mod.rs Normal file
View File

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

25
src/model/sighting.rs Normal file
View File

@@ -0,0 +1,25 @@
use crate::Backend;
use serde::{Deserialize, Serialize};
use sqlx::{types::chrono::NaiveDateTime, FromRow};
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Sighting {
pub user_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> {
match self {
Backend::Sqlite(db) => sqlx::query_as!(
Sighting,
"SELECT user_uuid, sighted_at, camera_id FROM sightings WHERE user_uuid = ?",
uuid
)
.fetch_all(db)
.await
.unwrap(),
}
}
}

0
src/model/user.rs Normal file
View File

View File

@@ -17,7 +17,7 @@ pub fn new(content: Markup) -> Markup {
}
ul {
li { a href="/" { "🏠" } }
li { a href="/cam" { "📸" } }
li { a href="/game" { "📸" } }
li { div id="theme_switcher" style="width: 20px; height: 20px;" {} }
}
}

6
test_db.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
rm -f db.sqlite
touch db.sqlite
sqlite3 db.sqlite < migration.sql