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]
|
||||
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"] }
|
||||
|
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::{
|
||||
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("—")) "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("—")) "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))
|
||||
|
16
src/main.rs
16
src/main.rs
@@ -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
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 {
|
||||
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
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