Compare commits

..

11 Commits

11 changed files with 573 additions and 292 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
.history
/frontend/node_modules/*
db.sqlite
config.toml

420
Cargo.lock generated
View File

@@ -17,6 +17,41 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -53,17 +88,6 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "async-trait"
version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atoi"
version = "2.0.0"
@@ -156,22 +180,6 @@ dependencies = [
"tower-service",
]
[[package]]
name = "axum-messages"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d67ce6e7bc1e1e71f2a4e86d418045a29c63c4ebb631f3d9bb2f81c4958ea391"
dependencies = [
"axum-core",
"http",
"parking_lot",
"serde",
"serde_json",
"tower",
"tower-sessions-core",
"tracing",
]
[[package]]
name = "backtrace"
version = "0.3.75"
@@ -290,6 +298,16 @@ dependencies = [
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -311,7 +329,11 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"aes-gcm",
"base64",
"percent-encoding",
"rand",
"subtle",
"time",
"version_check",
]
@@ -387,9 +409,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core",
"typenum",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "der"
version = "0.7.10"
@@ -408,7 +440,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
@@ -509,20 +540,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -567,17 +584,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
@@ -598,7 +604,6 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
@@ -640,6 +645,16 @@ dependencies = [
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "gimli"
version = "0.31.1"
@@ -661,8 +676,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
@@ -963,7 +978,7 @@ dependencies = [
"globset",
"log",
"memchr",
"regex-automata",
"regex-automata 0.4.9",
"same-file",
"walkdir",
"winapi-util",
@@ -979,6 +994,15 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "io-uring"
version = "0.7.9"
@@ -1061,7 +1085,6 @@ checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
"serde",
]
[[package]]
@@ -1070,6 +1093,15 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "matchit"
version = "0.8.4"
@@ -1161,6 +1193,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
@@ -1229,6 +1271,18 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking"
version = "2.2.1"
@@ -1312,6 +1366,18 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "polyval"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "potential_utf"
version = "0.1.2"
@@ -1419,8 +1485,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
@@ -1431,9 +1506,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -1524,7 +1605,7 @@ dependencies = [
"serde_json",
"serde_yaml",
"siphasher",
"toml",
"toml 0.8.23",
"triomphe",
]
@@ -1646,6 +1727,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -1693,6 +1783,15 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -2024,6 +2123,15 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.41"
@@ -2140,11 +2248,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_edit",
]
[[package]]
name = "toml"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [
"indexmap",
"serde",
"serde_spanned 1.0.0",
"toml_datetime 0.7.0",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
@@ -2154,6 +2277,15 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
@@ -2162,18 +2294,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_write",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
dependencies = [
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
[[package]]
name = "tower"
version = "0.5.2"
@@ -2190,22 +2337,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-cookies"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
dependencies = [
"axum-core",
"cookie",
"futures-util",
"http",
"parking_lot",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.6"
@@ -2244,57 +2375,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tower-sessions"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3"
dependencies = [
"async-trait",
"http",
"time",
"tokio",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions-core",
"tower-sessions-memory-store",
"tracing",
]
[[package]]
name = "tower-sessions-core"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b"
dependencies = [
"async-trait",
"axum-core",
"base64",
"futures",
"http",
"parking_lot",
"rand",
"serde",
"serde_json",
"thiserror",
"time",
"tokio",
"tracing",
]
[[package]]
name = "tower-sessions-memory-store"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242"
dependencies = [
"async-trait",
"time",
"tokio",
"tower-sessions-core",
]
[[package]]
name = "tracing"
version = "0.1.41"
@@ -2325,6 +2405,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -2377,6 +2487,16 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
@@ -2418,6 +2538,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -2543,15 +2669,17 @@ version = "0.1.0"
dependencies = [
"axum",
"axum-extra",
"axum-messages",
"chrono",
"maud",
"rust-i18n",
"serde",
"sqlx",
"time",
"tokio",
"toml 0.9.5",
"tower-http",
"tower-sessions",
"tracing",
"tracing-subscriber",
"uuid",
]
@@ -2565,6 +2693,22 @@ dependencies = [
"wasite",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
@@ -2574,6 +2718,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.2"

View File

@@ -5,14 +5,16 @@ edition = "2024"
[dependencies]
axum = "0.8"
axum-extra = { version = "0.10", features = ["cookie"] }
axum-messages = "0.8"
axum-extra = { version = "0.10", features = ["cookie-private", "cookie"] }
chrono = { version = "0.4", features = ["serde"] }
maud = { version = "0.27", features = ["axum"] }
rust-i18n = "3.1"
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "chrono"] }
time = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
toml = "0.9"
tower-http = { version = "0.6", features = ["fs"] }
tower-sessions = "0.14"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1.17", features = ["v4", "serde"] }

View File

@@ -29,6 +29,7 @@ location_linz: "Wo: überall in Linz"
game_title: "Wer findet die meisten Kameras?"
game_explanation_todo: "Willkommen zu unserem Überwachungsbewusstseinsspiel! Als Teil unserer Digital Shadows Ausstellung beim Ars Electronica Festival haben wir QR-Codes bei Überwachungskameras in ganz Linz platziert. Deine Mission: Entdecke die Kameras, scanne unsere Codes und finde heraus, wie allgegenwärtig öffentliche Überwachung wirklich ist. Wir sind aber nur Menschen wir haben nur einen kleinen Teil aller Kameras erfasst, die unsere Stadt beobachten. Wer beobachtet wen in unseren öffentlichen Räumen? Die Jagd beginnt jetzt! 🕵️"
save_button: "Speichern"
amount_participants: "In total there are %{amount} participants so far."
cameras_found: "Du hast %{found}/%{total} Kameras gefunden:"
highscore_title: "Bestenliste"
not_found_title: "ups"

View File

@@ -29,6 +29,7 @@ location_linz: "Where: all over Linz"
game_title: "Who finds the most cameras?"
game_explanation_todo: "Welcome to our public surveillance awareness game! As part of our Digital Shadows exhibition at Ars Electronica Festival, we've placed QR codes near surveillance cameras throughout Linz. Your mission: spot the cameras, scan our codes, and discover how pervasive public monitoring really is. We're only human though we've mapped just a small subset of all the cameras watching our city. Who's watching whom in our public spaces? The hunt begins now! 🕵️"
save_button: "Save"
amount_participants: "Aktuell gibt es insgesamt %{amount} Teilnehmer."
cameras_found: "You have found %{found}/%{total} cameras:"
highscore_title: "Highscore"
not_found_title: "uups"

View File

@@ -1,13 +1,16 @@
use crate::{language::language, page::Page, Backend, NameUpdateError};
use crate::{
language::language,
page::{MyMessage, Page},
AppState, Backend, NameUpdateError,
};
use axum::{
extract::{Path, State},
http::HeaderMap,
response::{IntoResponse, Redirect, Response},
response::{IntoResponse, Response},
routing::{get, post},
Form, Router,
};
use axum_extra::extract::CookieJar;
use axum_messages::Messages;
use axum_extra::extract::{CookieJar, PrivateCookieJar};
use maud::{html, Markup, PreEscaped};
use serde::Deserialize;
use std::sync::Arc;
@@ -15,23 +18,33 @@ use uuid::Uuid;
async fn index(
State(backend): State<Arc<Backend>>,
cookies: CookieJar,
messages: Messages,
cookies: PrivateCookieJar,
lang_cookies: CookieJar,
headers: HeaderMap,
) -> Response {
let lang = language(&cookies, &headers);
rust_i18n::set_locale(lang.to_locale());
retu(backend, cookies, lang_cookies, headers, None).await
}
let (cookies, req) = backend.client_full(cookies, &headers).await;
async fn retu(
backend: Arc<Backend>,
cookies: PrivateCookieJar,
lang_cookies: CookieJar,
headers: HeaderMap,
message: Option<MyMessage>,
) -> Response {
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
let client = req.client;
rust_i18n::set_locale(&req.lang.to_string());
let sightings = backend.sightings_for_client(&client).await;
let amount_total_cameras = backend.amount_total_cameras().await;
let highscore = backend.highscore().await;
let highscore = backend.highscore(&client).await;
let amount_participants = backend.amount_participants().await;
let lang = language(&cookies, &headers);
let mut page = Page::new(lang);
page.messages(messages);
let mut page = Page::new(req.lang);
if let Some(message) = message {
page.set_message(message);
}
let markup = page.content(html! {
hgroup {
h1 { (t!("game_title")) }
@@ -42,7 +55,7 @@ async fn index(
div.mb-sm { (t!("ask_to_change_name", name = client.get_display_name())) }
form action="/name" method="post" {
form action="/game" method="post" {
fieldset role="group" {
input
name="name"
@@ -73,6 +86,13 @@ async fn index(
h2 { (t!("highscore_title")) }
ul.iterated {
@for rank in highscore {
@if rank.show_dots_above {
li.card {
span {
""
}
}
}
li.card {
span {
span.font-headline.rank.text-muted { (rank.rank) "." }
@@ -88,6 +108,7 @@ async fn index(
}
}
}
(t!("amount_participants", amount = amount_participants))
}
});
@@ -96,36 +117,34 @@ async fn index(
async fn game(
State(backend): State<Arc<Backend>>,
cookies: CookieJar,
cookies: PrivateCookieJar,
lang_cookies: CookieJar,
headers: HeaderMap,
messages: Messages,
Path(uuid): Path<String>,
) -> Result<Redirect, Response> {
let lang = language(&cookies, &headers);
rust_i18n::set_locale(lang.to_locale());
let (cookies, client) = backend.client(cookies).await;
) -> Response {
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
let client = req.client;
rust_i18n::set_locale(req.lang.to_locale());
let Ok(uuid) = Uuid::parse_str(&uuid) else {
return Err(not_found(cookies, headers).await.into_response());
return not_found(lang_cookies, headers).await.into_response();
};
let Some(camera) = backend.get_camera(&uuid).await else {
return Err(not_found(cookies, headers).await.into_response());
return not_found(lang_cookies, headers).await.into_response();
};
if let Ok(number) = backend.client_found_camera(&client, &camera).await {
messages.info(format!("found-cam|{}|{number}", camera.name));
let message = if let Ok(number) = backend.client_found_camera(&client, &camera).await {
MyMessage::FoundCam(camera.name, number)
} else {
messages.info(format!(
"err|{}|{}|{}",
t!("error_already_found_title"),
t!("error_already_found_body"),
t!("error_already_found_footer")
));
}
MyMessage::Error(
t!("error_already_found_title").into(),
t!("error_already_found_body").into(),
t!("error_already_found_footer").into(),
)
};
Ok(Redirect::to("/game"))
retu(backend, cookies, lang_cookies, headers, Some(message)).await
}
async fn not_found(cookies: CookieJar, headers: HeaderMap) -> Markup {
@@ -142,47 +161,40 @@ struct NameForm {
async fn set_name(
State(backend): State<Arc<Backend>>,
cookies: CookieJar,
messages: Messages,
cookies: PrivateCookieJar,
lang_cookies: CookieJar,
headers: HeaderMap,
Form(form): Form<NameForm>,
) -> Response {
let lang = language(&cookies, &headers);
rust_i18n::set_locale(lang.to_locale());
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
let client = req.client;
rust_i18n::set_locale(req.lang.to_locale());
let (cookies, client) = backend.client(cookies).await;
match backend.set_client_name(&client, &form.name).await {
Ok(()) => messages.info("set-name-succ"),
Err(NameUpdateError::TooShort(expected, actual)) => messages.info(format!(
"err|{}|{}|{}: {}",
t!("error_name_too_short_title"),
t!("error_name_too_short_body", expected = expected),
t!("received_characters"),
actual
)),
Err(NameUpdateError::TooLong(expected, actual)) => messages.info(format!(
"err|{}|{}|{}: {}",
t!("error_name_too_long_title"),
t!("error_name_too_long_body", expected = expected),
t!("received_characters"),
actual
)),
Err(NameUpdateError::ContainsBadWord) => messages.info(format!(
"err|{}|{}|{}",
t!("error_bad_word_title"),
t!("error_bad_word_body"),
t!("error_bad_word_footer")
)),
let message = match backend.set_client_name(&client, &form.name).await {
Ok(()) => MyMessage::NameChanged,
Err(NameUpdateError::TooShort(expected, actual)) => MyMessage::Error(
t!("error_name_too_short_title").into(),
t!("error_name_too_short_body", expected = expected).into(),
format!("{}: {actual}", t!("received_characters")),
),
Err(NameUpdateError::TooLong(expected, actual)) => MyMessage::Error(
t!("error_name_too_long_title").into(),
t!("error_name_too_long_body", expected = expected).into(),
format!("{}: {actual}", t!("received_characters")),
),
Err(NameUpdateError::ContainsBadWord) => MyMessage::Error(
t!("error_bad_word_title").into(),
t!("error_bad_word_body").into(),
t!("error_bad_word_footer").into(),
),
};
// Redirect back to the game page
(cookies, Redirect::to("/game")).into_response()
retu(backend, cookies, lang_cookies, headers, Some(message)).await
}
pub(super) fn routes() -> Router<Arc<Backend>> {
pub(super) fn routes() -> Router<AppState> {
Router::new()
.route("/game", get(index))
.route("/name", post(set_name))
.route("/game", post(set_name))
.route("/{*uuid}", get(game))
}

View File

@@ -1,7 +1,7 @@
use crate::{language::language, page::Page};
use axum::http::HeaderMap;
use axum_extra::extract::CookieJar;
use maud::{html, Markup, PreEscaped};
use maud::{Markup, PreEscaped, html};
pub(super) async fn index(cookies: CookieJar, headers: HeaderMap) -> Markup {
let lang = language(&cookies, &headers);

View File

@@ -1,16 +1,22 @@
use crate::model::client::Client;
use axum::{http::HeaderMap, routing::get, Router};
use axum_extra::extract::{cookie::Cookie, CookieJar};
use axum_messages::MessagesManagerLayer;
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, SqlitePool};
use axum::{Router, http::HeaderMap, routing::get};
use axum_extra::extract::{
CookieJar, PrivateCookieJar,
cookie::{Cookie, Expiration, Key},
};
use serde::{Deserialize, Serialize};
use sqlx::{SqlitePool, pool::PoolOptions, sqlite::SqliteConnectOptions};
use std::{
collections::HashSet,
fmt::Display,
fs,
path::Path,
str::FromStr,
sync::{Arc, LazyLock},
};
use time::{Duration, OffsetDateTime};
use tower_http::services::ServeDir;
use tower_sessions::{MemoryStore, SessionManagerLayer};
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
use uuid::Uuid;
#[macro_use]
@@ -70,6 +76,7 @@ impl From<String> for Language {
}
}
#[derive(Debug)]
struct Req {
client: Client,
lang: Language,
@@ -140,7 +147,7 @@ mod tests {
}
impl Backend {
async fn client(&self, cookies: CookieJar) -> (CookieJar, Client) {
async fn client(&self, cookies: PrivateCookieJar) -> (PrivateCookieJar, Client) {
let existing_uuid = cookies
.get("client_id")
.and_then(|cookie| Uuid::parse_str(cookie.value()).ok());
@@ -149,16 +156,27 @@ impl Backend {
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()));
let expiration_date = OffsetDateTime::now_utc() + Duration::days(30);
let mut cookie = Cookie::new("client_id", new_id.to_string());
cookie.set_expires(Expiration::DateTime(expiration_date));
cookie.set_http_only(true);
cookie.set_secure(true);
let updated_cookies = cookies.add(cookie);
(updated_cookies, self.get_client(&new_id).await)
}
}
}
// Combined method for getting both client and language
async fn client_full(&self, cookies: CookieJar, headers: &HeaderMap) -> (CookieJar, Req) {
async fn client_full(
&self,
cookies: PrivateCookieJar,
lang_cookies: &CookieJar,
headers: &HeaderMap,
) -> (PrivateCookieJar, Req) {
let (cookies, client) = self.client(cookies).await;
let lang = language::language(&cookies, headers);
let lang = language::language(&lang_cookies, headers);
(cookies, Req { client, lang })
}
@@ -190,10 +208,61 @@ impl Backend {
}
}
#[derive(Clone)]
pub struct AppState {
pub(crate) backend: Arc<Backend>,
pub key: Key,
}
impl axum::extract::FromRef<AppState> for Key {
fn from_ref(state: &AppState) -> Self {
state.key.clone()
}
}
impl axum::extract::FromRef<AppState> for Arc<Backend> {
fn from_ref(state: &AppState) -> Self {
state.backend.clone()
}
}
#[derive(Serialize, Deserialize)]
struct Config {
key: Vec<u8>,
}
impl Config {
fn generate() -> Self {
Self {
key: Key::generate().master().to_vec(),
}
}
}
fn load_or_create_key() -> Result<Key, Box<dyn std::error::Error>> {
let config_path = "config.toml";
// Try to read existing config
if Path::new(config_path).exists() {
let content = fs::read_to_string(config_path)?;
let config: Config = toml::from_str(&content)?;
return Ok(Key::from(&config.key));
}
// Create new config if file doesn't exist
let config = Config::generate();
let toml_string = toml::to_string(&config)?;
fs::write(config_path, toml_string)?;
Ok(Key::from(&config.key))
}
#[tokio::main]
async fn main() {
let session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store).with_secure(false);
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(EnvFilter::from_default_env())
.init();
let connection_options = SqliteConnectOptions::from_str("sqlite://db.sqlite").unwrap();
let db: SqlitePool = PoolOptions::new()
@@ -201,13 +270,17 @@ async fn main() {
.await
.unwrap();
let key = load_or_create_key().unwrap();
let state = AppState {
backend: Arc::new(Backend::Sqlite(db)),
key,
};
let app = Router::new()
.route("/", get(index::index))
.nest_service("/static", ServeDir::new("./static/serve"))
.merge(game::routes())
.with_state(Arc::new(Backend::Sqlite(db)))
.layer(MessagesManagerLayer)
.layer(session_layer);
.with_state(state);
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();

View File

@@ -1,40 +1,102 @@
use crate::{Backend, model::client::Client};
use crate::{model::client::Client, Backend};
pub(crate) struct Rank {
pub(crate) rank: i64,
pub(crate) client: Client,
pub(crate) amount: i64,
pub(crate) show_dots_above: bool,
}
impl Backend {
pub(crate) async fn highscore(&self) -> Vec<Rank> {
pub(crate) async fn amount_participants(&self) -> i64 {
match self {
Backend::Sqlite(db) => {
let row = sqlx::query!("SELECT COUNT(*) as count FROM client")
.fetch_one(db)
.await
.unwrap();
row.count
}
}
}
pub(crate) async fn highscore(&self, client: &Client) -> Vec<Rank> {
match self {
Backend::Sqlite(db) => {
let rows = sqlx::query!(
"SELECT
RANK() OVER (ORDER BY COUNT(s.client_uuid) DESC) as rank,
"WITH ranked_clients AS (
SELECT
DENSE_RANK() OVER (ORDER BY COUNT(s.client_uuid) DESC) as rank,
c.name,
c.uuid,
COUNT(s.client_uuid) as amount
FROM client c
LEFT JOIN sightings s ON c.uuid = s.client_uuid
GROUP BY c.uuid, c.name
ORDER BY amount DESC"
)
SELECT rank, name, uuid, amount
FROM ranked_clients
WHERE rank <= (
SELECT rank
FROM ranked_clients
ORDER BY rank
LIMIT 1 OFFSET 9
)
ORDER BY rank, name"
)
.fetch_all(db)
.await
.unwrap_or_default();
rows.into_iter()
let mut ret: Vec<Rank> = rows
.into_iter()
.map(|row| Rank {
rank: row.rank.unwrap(),
client: Client {
uuid: row.uuid.unwrap(),
name: row.name,
},
amount: row.amount.unwrap(),
show_dots_above: false,
})
.collect();
let user_is_in_top = ret.iter().find(|x| &x.client == client).is_some();
if !user_is_in_top {
let row = sqlx::query!(
"WITH ranked_clients AS (
SELECT
DENSE_RANK() OVER (ORDER BY COUNT(s.client_uuid) DESC) as rank,
c.name,
c.uuid,
COUNT(s.client_uuid) as amount
FROM client c
LEFT JOIN sightings s ON c.uuid = s.client_uuid
GROUP BY c.uuid, c.name
)
SELECT rank, name, uuid, amount
FROM ranked_clients
WHERE uuid = ?
ORDER BY rank, name",
client.uuid
)
.fetch_one(db)
.await
.unwrap();
ret.push(Rank {
rank: row.rank,
client: Client {
uuid: row.uuid,
name: row.name,
},
amount: row.amount,
show_dots_above: true,
})
.collect()
}
ret
}
}
}

View File

@@ -1,52 +1,27 @@
use crate::Language;
use axum_messages::Messages;
use maud::{DOCTYPE, Markup, html};
pub(crate) struct Page {
lang: Language,
found_camera: Option<(String, i64)>,
new_name: bool,
err: Option<(String, String, String)>,
message: Option<MyMessage>,
}
pub(crate) enum MyMessage {
NameChanged,
FoundCam(String, i64),
Error(String, String, String),
}
impl Page {
pub fn new(lang: Language) -> Self {
Self {
lang,
found_camera: None,
new_name: false,
err: None,
message: None,
}
}
pub fn messages(&mut self, messages: Messages) {
for message in messages {
let text = &message.to_string()[..];
match (message.level, text) {
(_, "set-name-succ") => {
self.new_name = true;
}
(_, msg) if msg.starts_with("found-cam|") => {
let mut parts = msg.splitn(3, '|');
let _ = parts.next().expect("just checked |");
if let (Some(name), Some(amount)) = (parts.next(), parts.next()) {
if let Ok(amount) = amount.parse::<i64>() {
self.found_camera = Some((name.into(), amount));
}
}
}
(_, msg) if msg.starts_with("err|") => {
let mut parts = msg.splitn(4, '|');
let _ = parts.next().expect("just checked |");
if let (Some(title), Some(body), Some(footer)) =
(parts.next(), parts.next(), parts.next())
{
self.err = Some((title.into(), body.into(), footer.into()));
}
}
(_, _) => {}
}
}
pub(crate) fn set_message(&mut self, message: MyMessage) {
self.message = Some(message);
}
pub fn content(self, content: Markup) -> Markup {
@@ -92,35 +67,39 @@ impl Page {
}
main.container {
@if let Some(found_camera) = &self.found_camera {
@if let Some(message) = &self.message {
@match message {
MyMessage::FoundCam(name, amount) => {
div.flex {
article class="succ msg" {
header { (t!("found_camera_title", name = found_camera.0)) }
(t!("found_camera_body", amount = found_camera.1))
header { (t!("found_camera_title", name = name)) }
(t!("found_camera_body", amount = amount))
footer {
a href="#ranking" { (t!("see_ranking")) }
}
}
}
}
@if self.new_name {
},
MyMessage::NameChanged => {
div.flex {
article class="name msg" {
header { (t!("new_name_title")) }
(t!("new_name_message"))
}
}
}
@if let Some(err) = &self.err {
},
MyMessage::Error(header, body, footer) => {
div.flex {
article class="error msg" {
header { (err.0) }
(err.1)
footer { (err.2) }
header { (header) }
(body)
footer { (footer) }
}
}
}
}
}
section { (content) }
}

View File

@@ -13,7 +13,7 @@ function setLanguageCookie() {
// set lang, if lang attribute doesn't exit set default en
let lang = langToggle.getAttribute('lang') ? langToggle.getAttribute('lang') : 'en';
document.cookie = "language=" + lang;
window.location.reload();
window.location.assign(window.location.href);
})
}