add sqlite db; add todos :-(; show current cameras
This commit is contained in:
1522
Cargo.lock
generated
1522
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -6,7 +6,10 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
axum-extra = { version = "0.10", features = ["cookie"] }
|
axum-extra = { version = "0.10", features = ["cookie"] }
|
||||||
maud = { version = "0.27.0", features = ["axum"] }
|
chrono = { version = "0.4.41", features = ["serde"] }
|
||||||
tokio = { version = "1.47.0", features = ["macros", "rt-multi-thread"] }
|
maud = { version = "0.27", features = ["axum"] }
|
||||||
tower-http = { version = "0.6.6", features = ["fs"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
uuid = { version = "1.17.0", features = ["v4", "serde"] }
|
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"] }
|
||||||
|
26
migration.sql
Normal file
26
migration.sql
Normal 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
2
src/backend.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
46
src/game.rs
46
src/game.rs
@@ -1,29 +1,51 @@
|
|||||||
use crate::{client_id, page::new};
|
use crate::{client_id, page::new, Backend};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::Path,
|
extract::{Path, State},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use maud::{html, Markup, PreEscaped};
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
fn page(content: Markup) -> Markup {
|
async fn index(State(backend): State<Arc<Backend>>, cookies: CookieJar) -> Response {
|
||||||
new(html! {
|
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 {
|
hgroup {
|
||||||
h1 { "Digital Shadows" (PreEscaped("—")) "Who finds the most cameras?" }
|
h1 { "Digital Shadows" (PreEscaped("—")) "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 {
|
(cookies, markup).into_response()
|
||||||
page(html! {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn game(cookies: CookieJar, Path(uuid): Path<String>) -> 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 {
|
let Ok(uuid) = Uuid::parse_str(&uuid) else {
|
||||||
return not_found().await.into_response();
|
return not_found().await.into_response();
|
||||||
@@ -33,7 +55,7 @@ async fn game(cookies: CookieJar, Path(uuid): Path<String>) -> Response {
|
|||||||
hgroup {
|
hgroup {
|
||||||
h1 { "Digital Shadows" (PreEscaped("—")) "Who finds the most cameras?" }
|
h1 { "Digital Shadows" (PreEscaped("—")) "Who finds the most cameras?" }
|
||||||
h2 {
|
h2 {
|
||||||
(device)
|
(client)
|
||||||
" found camera "
|
" found camera "
|
||||||
(uuid)
|
(uuid)
|
||||||
}
|
}
|
||||||
@@ -49,7 +71,7 @@ async fn not_found() -> Markup {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn routes() -> Router {
|
pub(super) fn routes() -> Router<Arc<Backend>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/game", get(index))
|
.route("/game", get(index))
|
||||||
.route("/{*uuid}", get(game))
|
.route("/{*uuid}", get(game))
|
||||||
|
16
src/main.rs
16
src/main.rs
@@ -1,12 +1,19 @@
|
|||||||
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 std::{str::FromStr, sync::Arc};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
mod game;
|
mod game;
|
||||||
mod index;
|
mod index;
|
||||||
|
pub(crate) mod model;
|
||||||
mod page;
|
mod page;
|
||||||
|
|
||||||
|
pub(crate) enum Backend {
|
||||||
|
Sqlite(SqlitePool),
|
||||||
|
}
|
||||||
|
|
||||||
fn client_id(cookies: CookieJar) -> (CookieJar, String) {
|
fn client_id(cookies: CookieJar) -> (CookieJar, String) {
|
||||||
let mut cookies = cookies;
|
let mut cookies = cookies;
|
||||||
if cookies.get("client_id").is_none() {
|
if cookies.get("client_id").is_none() {
|
||||||
@@ -24,10 +31,17 @@ fn client_id(cookies: CookieJar) -> (CookieJar, String) {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn 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()
|
let app = Router::new()
|
||||||
.route("/", get(index::index))
|
.route("/", get(index::index))
|
||||||
.nest_service("/static", ServeDir::new("./static/serve"))
|
.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
|
// run our app with hyper, listening globally on port 3000
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||||
|
24
src/model/camera.rs
Normal file
24
src/model/camera.rs
Normal 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
3
src/model/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub(crate) mod camera;
|
||||||
|
pub(crate) mod sighting;
|
||||||
|
pub(crate) mod user;
|
25
src/model/sighting.rs
Normal file
25
src/model/sighting.rs
Normal 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
0
src/model/user.rs
Normal file
@@ -17,7 +17,7 @@ pub fn new(content: Markup) -> Markup {
|
|||||||
}
|
}
|
||||||
ul {
|
ul {
|
||||||
li { a href="/" { "🏠" } }
|
li { a href="/" { "🏠" } }
|
||||||
li { a href="/cam" { "📸" } }
|
li { a href="/game" { "📸" } }
|
||||||
li { div id="theme_switcher" style="width: 20px; height: 20px;" {} }
|
li { div id="theme_switcher" style="width: 20px; height: 20px;" {} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
test_db.sh
Executable file
6
test_db.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
rm -f db.sqlite
|
||||||
|
touch db.sqlite
|
||||||
|
sqlite3 db.sqlite < migration.sql
|
||||||
|
|
Reference in New Issue
Block a user