From 03b614c62e0c93dd407780b5476bcb5e841ec79a Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Wed, 22 Apr 2026 21:36:56 +0200 Subject: [PATCH] =?UTF-8?q?refact:=E2=80=AFmigrate=20sqlx=20+=20sqlite=20t?= =?UTF-8?q?o=20tokio-postgresql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 607 ++++++++++++++++++-- Cargo.toml | 10 + devenv.lock | 12 +- devenv.nix | 11 +- server/relay-server/Cargo.toml | 10 +- server/relay-server/GameConfig.json | 10 +- server/relay-server/migrations/001_init.sql | 28 +- server/relay-server/src/auth.rs | 8 +- server/relay-server/src/db.rs | 283 +++++---- server/relay-server/src/http.rs | 87 ++- server/relay-server/src/lobby.rs | 8 +- server/relay-server/src/main.rs | 14 +- 12 files changed, 838 insertions(+), 250 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2141f27..59140f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -191,6 +191,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash 0.5.0", +] + [[package]] name = "arimaa_engine_step" version = "1.0.1" @@ -378,7 +390,7 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -411,7 +423,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http", + "http 1.4.0", "http-body", "http-body-util", "mime", @@ -422,6 +434,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-login" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964ea6eb764a227baa8c3368e45c94d23b6863cc7b880c6c9e341c143c5a5ff7" +dependencies = [ + "axum", + "form_urlencoded", + "serde", + "subtle", + "thiserror 2.0.18", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions", + "tracing", + "urlencoding", +] + [[package]] name = "backbone-lib" version = "0.1.0" @@ -554,6 +585,15 @@ dependencies = [ "core2", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "block" version = "0.1.6" @@ -569,6 +609,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "board-game" version = "0.8.2" @@ -1359,7 +1408,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", "zeroize", ] @@ -1428,6 +1477,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "cobs" version = "0.3.0" @@ -1548,6 +1603,12 @@ dependencies = [ "toml 0.8.23", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -1843,6 +1904,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csv" version = "1.4.0" @@ -1864,6 +1934,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "cubecl" version = "0.9.0" @@ -2412,6 +2491,41 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-postgres" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d697d376cbfa018c23eb4caab1fd1883dd9c906a8c034e8d9a3cb06a7e0bef9" +dependencies = [ + "async-trait", + "deadpool", + "getrandom 0.2.17", + "tokio", + "tokio-postgres", + "tracing", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + [[package]] name = "decorum" version = "0.3.1" @@ -2440,6 +2554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -2516,11 +2631,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "directories" version = "6.0.0" @@ -2868,6 +2995,12 @@ dependencies = [ "synstructure 0.12.6", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -3292,7 +3425,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -3435,6 +3568,27 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "gloo-net" version = "0.6.0" @@ -3445,7 +3599,7 @@ dependencies = [ "futures-core", "futures-sink", "gloo-utils", - "http", + "http 1.4.0", "js-sys", "pin-project", "serde", @@ -3602,7 +3756,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.4.0", "indexmap", "slab", "tokio", @@ -3728,7 +3882,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", ] [[package]] @@ -3740,6 +3903,17 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.4.0" @@ -3757,7 +3931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.0", ] [[package]] @@ -3768,11 +3942,17 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", + "http 1.4.0", "http-body", "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + [[package]] name = "httparse" version = "1.10.1" @@ -3791,6 +3971,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hydration_context" version = "0.2.1" @@ -3816,7 +4005,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http", + "http 1.4.0", "http-body", "httparse", "httpdate", @@ -3833,7 +4022,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http", + "http 1.4.0", "hyper", "hyper-util", "rustls", @@ -3854,7 +4043,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.4.0", "http-body", "hyper", "ipnet", @@ -4778,6 +4967,42 @@ dependencies = [ "web-sys", ] +[[package]] +name = "leptos_router" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4168ead6a9715daba953aa842795cb2ad81b6e011a15745bd3d1baf86f76de95" +dependencies = [ + "any_spawner", + "either_of", + "futures", + "gloo-net 0.6.0", + "js-sys", + "leptos", + "leptos_router_macro", + "once_cell", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "tachys", + "thiserror 2.0.18", + "url", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router_macro" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31197af38d209ffc5d9f89715381c415a1570176f8d23455fbe00d148e79640" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "leptos_server" version = "0.7.8" @@ -4917,6 +5142,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", + "serde", ] [[package]] @@ -5047,6 +5273,16 @@ dependencies = [ "rayon", ] +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.2", +] + [[package]] name = "md5" version = "0.8.0" @@ -5112,6 +5348,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -5136,7 +5382,7 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -5493,6 +5739,15 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.37.3" @@ -5614,6 +5869,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -5638,10 +5904,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest", - "hmac", - "password-hash", - "sha2", + "digest 0.10.7", + "hmac 0.12.1", + "password-hash 0.4.2", + "sha2 0.10.9", ] [[package]] @@ -5690,7 +5956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -5703,6 +5969,25 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -5830,6 +6115,35 @@ dependencies = [ "serde", ] +[[package]] +name = "postgres-protocol" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "hmac 0.13.0", + "md-5", + "memchr", + "rand 0.10.1", + "sha2 0.11.0", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" +dependencies = [ + "bytes", + "fallible-iterator 0.2.0", + "postgres-protocol", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -6557,6 +6871,31 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "relay-server" +version = "0.1.0" +dependencies = [ + "argon2", + "axum", + "axum-login", + "bytes", + "deadpool-postgres", + "futures-util", + "postcard", + "protocol", + "rand 0.8.5", + "serde", + "serde_json", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-postgres", + "tower-http", + "tower-sessions", + "tracing", + "tracing-subscriber", +] + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -6597,7 +6936,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -6722,7 +7061,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ "bitflags 2.11.0", - "fallible-iterator", + "fallible-iterator 0.3.0", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", @@ -7077,8 +7416,8 @@ dependencies = [ "const_format", "dashmap", "futures", - "gloo-net", - "http", + "gloo-net 0.6.0", + "http 1.4.0", "js-sys", "once_cell", "pin-project-lite", @@ -7129,7 +7468,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -7140,7 +7479,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -7227,6 +7577,12 @@ dependencies = [ "quote", ] +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -7333,6 +7689,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -7788,6 +8155,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -7807,6 +8175,32 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-postgres" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.10.1", + "socket2", + "tokio", + "tokio-util", + "whoami", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -7987,6 +8381,22 @@ 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 1.4.0", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.6.8" @@ -7995,14 +8405,24 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags 2.11.0", "bytes", + "futures-core", "futures-util", - "http", + "http 1.4.0", "http-body", + "http-body-util", + "http-range-header", + "httpdate", "iri-string", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -8017,6 +8437,57 @@ 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 1.4.0", + "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 0.22.1", + "futures", + "http 1.4.0", + "parking_lot", + "rand 0.8.5", + "serde", + "serde_json", + "thiserror 2.0.18", + "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 = "tracel-llvm" version = "20.1.4-7" @@ -8042,7 +8513,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tar", "walkdir", ] @@ -8243,7 +8714,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.4.0", "httparse", "log", "rand 0.8.5", @@ -8260,7 +8731,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http", + "http 1.4.0", "httparse", "log", "rand 0.9.3", @@ -8322,6 +8793,18 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -8337,6 +8820,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.13.2" @@ -8390,7 +8879,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -8442,6 +8931,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -8561,6 +9056,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -8579,6 +9083,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + [[package]] name = "wasm-bindgen" version = "0.2.118" @@ -8701,6 +9214,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-user-portal" +version = "0.1.0" +dependencies = [ + "gloo-net 0.5.0", + "js-sys", + "leptos", + "leptos_router", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -8874,6 +9402,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "whoami" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + [[package]] name = "wide" version = "0.7.33" @@ -9607,7 +10148,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "flate2", - "hmac", + "hmac 0.12.1", "pbkdf2", "sha1", "time", diff --git a/Cargo.toml b/Cargo.toml index 8730e14..c0c930c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,16 @@ members = [ "spiel_bot", ] +default-members = [ + "store", + "clients/cli", + "clients/backbone-lib", + "server/protocol", + "server/relay-server", + "bot", + "spiel_bot", +] + # For the server we will need opt-level='3' [profile.release] opt-level = 'z' # Minimum space diff --git a/devenv.lock b/devenv.lock index 37fe4a9..0f2de9a 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1770390537, + "lastModified": 1776863933, "owner": "cachix", "repo": "devenv", - "rev": "d6f45cc00829254a9a6f8807c8fbfaf3efa7e629", + "rev": "863b4204725efaeeb73811e376f928232b720646", "type": "github" }, "original": { @@ -40,10 +40,10 @@ ] }, "locked": { - "lastModified": 1769939035, + "lastModified": 1776796298, "owner": "cachix", "repo": "git-hooks.nix", - "rev": "a8ca480175326551d6c4121498316261cbb5b260", + "rev": "3cfd774b0a530725a077e17354fbdb87ea1c4aad", "type": "github" }, "original": { @@ -74,10 +74,10 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770136044, + "lastModified": 1776734388, "owner": "NixOS", "repo": "nixpkgs", - "rev": "e576e3c9cf9bad747afcddd9e34f51d18c855b4e", + "rev": "10e7ad5bbcb421fe07e3a4ad53a634b0cd57ffac", "type": "github" }, "original": { diff --git a/devenv.nix b/devenv.nix index bc23f24..b2467ab 100644 --- a/devenv.nix +++ b/devenv.nix @@ -8,7 +8,9 @@ in # for Leptos pkgs.trunk pkgs.lld - # pkgs.wasm-bindgen-cli_0_2_114 + + # for backbone-lib + pkgs.wasm-bindgen-cli_0_2_114 pkgs.binaryen # for wasm-opt # pour burn-rs @@ -25,6 +27,13 @@ in ]; + services.postgres = { + enable = true; + listen_addresses = "*"; + # port = 5432; + initialDatabases = [{ name = "trictrac"; user = "trictrac"; pass = "trictrac"; }]; + }; + # https://devenv.sh/languages/ languages.rust.enable = true; diff --git a/server/relay-server/Cargo.toml b/server/relay-server/Cargo.toml index 417ce36..8ff17bd 100644 --- a/server/relay-server/Cargo.toml +++ b/server/relay-server/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -tokio = {version = "1.48.0", features = ["full"]} +tokio = { version = "1.48.0", features = ["full"] } axum = { version = "0.8.7", features = ["ws"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = { version = "1.0.228", features = ["derive"] } @@ -14,16 +14,14 @@ postcard = "1.1.3" bytes = "1.11.0" tracing = "0.1.41" tower-http = { version = "0.6.7", features = ["fs", "cors"] } -protocol = {path = "../protocol"} +protocol = { path = "../protocol" } rand = "0.8" # User management / auth -sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "migrate"] } +tokio-postgres = "0.7" +deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] } tower-sessions = "0.14" -tower-sessions-sqlx-store = { version = "0.15", features = ["sqlite"] } axum-login = "0.18" argon2 = "0.5" time = "0.3" thiserror = "1" - - diff --git a/server/relay-server/GameConfig.json b/server/relay-server/GameConfig.json index 386137b..8001497 100644 --- a/server/relay-server/GameConfig.json +++ b/server/relay-server/GameConfig.json @@ -1,10 +1,6 @@ [ { - "name" : "tic-tac-toe", - "max_players" : 10 - }, - { - "name" : "Ternio", - "max_players" : 3 + "name": "trictrac", + "max_players": 10 } -] \ No newline at end of file +] diff --git a/server/relay-server/migrations/001_init.sql b/server/relay-server/migrations/001_init.sql index ff53de0..0f75f53 100644 --- a/server/relay-server/migrations/001_init.sql +++ b/server/relay-server/migrations/001_init.sql @@ -1,24 +1,24 @@ CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL UNIQUE, - email TEXT NOT NULL UNIQUE, - password_hash TEXT NOT NULL, - created_at INTEGER NOT NULL + id BIGSERIAL PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS game_records ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - game_id TEXT NOT NULL, - room_code TEXT NOT NULL, - started_at INTEGER NOT NULL, - ended_at INTEGER, + id BIGSERIAL PRIMARY KEY, + game_id TEXT NOT NULL, + room_code TEXT NOT NULL, + started_at BIGINT NOT NULL, + ended_at BIGINT, result TEXT ); CREATE TABLE IF NOT EXISTS game_participants ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - game_record_id INTEGER NOT NULL REFERENCES game_records(id), - user_id INTEGER REFERENCES users(id), - player_id INTEGER NOT NULL, + id BIGSERIAL PRIMARY KEY, + game_record_id BIGINT NOT NULL REFERENCES game_records(id), + user_id BIGINT REFERENCES users(id), + player_id BIGINT NOT NULL, outcome TEXT ); diff --git a/server/relay-server/src/auth.rs b/server/relay-server/src/auth.rs index 1b00bda..f252f11 100644 --- a/server/relay-server/src/auth.rs +++ b/server/relay-server/src/auth.rs @@ -7,7 +7,7 @@ use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, Salt use argon2::password_hash::rand_core::OsRng; use argon2::Argon2; use axum_login::{AuthUser, AuthnBackend, UserId}; -use sqlx::SqlitePool; +use deadpool_postgres::Pool; use crate::db; @@ -39,7 +39,7 @@ pub struct Credentials { #[derive(Debug, thiserror::Error)] pub enum AuthError { #[error("database error: {0}")] - Database(#[from] sqlx::Error), + Database(#[from] db::DbError), #[error("password hashing error")] PasswordHash, } @@ -48,11 +48,11 @@ pub enum AuthError { #[derive(Clone)] pub struct AuthBackend { - pool: SqlitePool, + pool: Pool, } impl AuthBackend { - pub fn new(pool: SqlitePool) -> Self { + pub fn new(pool: Pool) -> Self { Self { pool } } } diff --git a/server/relay-server/src/db.rs b/server/relay-server/src/db.rs index 1196d7e..9f64a1f 100644 --- a/server/relay-server/src/db.rs +++ b/server/relay-server/src/db.rs @@ -1,14 +1,14 @@ //! Database access layer. //! -//! All SQLite interaction is funnelled through this module. Functions return -//! `sqlx::Result` so callers can handle errors uniformly. +//! All PostgreSQL interaction is funnelled through this module. Functions return +//! `Result<_, DbError>` so callers can handle errors uniformly. -use sqlx::sqlite::SqliteConnectOptions; -use sqlx::{SqlitePool, pool::PoolOptions}; +use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod}; +use tokio_postgres::{NoTls, error::SqlState}; use std::time::{SystemTime, UNIX_EPOCH}; /// A registered user as stored in the database. -#[derive(Clone, Debug, sqlx::FromRow)] +#[derive(Clone, Debug)] pub struct User { pub id: i64, pub username: String, @@ -18,7 +18,6 @@ pub struct User { } /// Aggregated game statistics for a user's public profile. -#[derive(sqlx::FromRow)] pub struct UserStats { pub total: i64, pub wins: i64, @@ -27,7 +26,6 @@ pub struct UserStats { } /// A condensed game entry returned by [`get_user_games`]. -#[derive(sqlx::FromRow)] pub struct GameSummary { pub id: i64, pub game_id: String, @@ -38,6 +36,24 @@ pub struct GameSummary { pub outcome: Option, } +#[derive(Debug, thiserror::Error)] +pub enum DbError { + #[error("connection pool error: {0}")] + Pool(#[from] deadpool_postgres::PoolError), + #[error("database error: {0}")] + Db(#[from] tokio_postgres::Error), +} + +impl DbError { + pub fn is_unique_violation(&self) -> bool { + if let DbError::Db(e) = self { + e.code() == Some(&SqlState::UNIQUE_VIOLATION) + } else { + false + } + } +} + fn now_unix() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -45,34 +61,28 @@ fn now_unix() -> i64 { .as_secs() as i64 } -/// Opens (or creates) the SQLite database at `path` and runs all pending migrations. -pub async fn init_db(path: &str) -> SqlitePool { - if let Some(parent) = std::path::Path::new(path).parent() { - if !parent.as_os_str().is_empty() { - tokio::fs::create_dir_all(parent) - .await - .expect("Failed to create database directory"); - } - } +/// Connects to the PostgreSQL database at `url` and runs all pending migrations. +pub async fn init_db(url: &str) -> Pool { + let pg_config: tokio_postgres::Config = url.parse().expect("Invalid DATABASE_URL"); + let manager = Manager::from_config( + pg_config, + NoTls, + ManagerConfig { recycling_method: RecyclingMethod::Fast }, + ); + let pool = Pool::builder(manager) + .max_size(5) + .build() + .expect("Failed to build connection pool"); - let pool = PoolOptions::::new() - .max_connections(5) - .connect_with( - SqliteConnectOptions::new() - .filename(path) - .create_if_missing(true), - ) + let client = pool.get().await.expect("Failed to get connection for migrations"); + client + .batch_execute(include_str!("../migrations/001_init.sql")) .await - .expect("Failed to open SQLite database"); - - sqlx::migrate::Migrator::new( - std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations")), - ) - .await - .expect("Failed to locate migrations directory") - .run(&pool) - .await - .expect("Failed to run database migrations"); + .expect("Migration 001 failed"); + client + .batch_execute(include_str!("../migrations/002_participants_unique.sql")) + .await + .expect("Migration 002 failed"); pool } @@ -80,135 +90,164 @@ pub async fn init_db(path: &str) -> SqlitePool { // ── Users ──────────────────────────────────────────────────────────────────── pub async fn create_user( - pool: &SqlitePool, + pool: &Pool, username: &str, email: &str, password_hash: &str, -) -> sqlx::Result { - let id = sqlx::query( - "INSERT INTO users (username, email, password_hash, created_at) VALUES (?, ?, ?, ?)", - ) - .bind(username) - .bind(email) - .bind(password_hash) - .bind(now_unix()) - .execute(pool) - .await? - .last_insert_rowid(); - Ok(id) +) -> Result { + let client = pool.get().await?; + let row = client + .query_one( + "INSERT INTO users (username, email, password_hash, created_at) \ + VALUES ($1, $2, $3, $4) RETURNING id", + &[&username, &email, &password_hash, &now_unix()], + ) + .await?; + Ok(row.get(0)) } -pub async fn get_user_by_id(pool: &SqlitePool, id: i64) -> sqlx::Result> { - sqlx::query_as::<_, User>( - "SELECT id, username, email, password_hash, created_at FROM users WHERE id = ?", - ) - .bind(id) - .fetch_optional(pool) - .await +pub async fn get_user_by_id(pool: &Pool, id: i64) -> Result, DbError> { + let client = pool.get().await?; + let row = client + .query_opt( + "SELECT id, username, email, password_hash, created_at FROM users WHERE id = $1", + &[&id], + ) + .await?; + Ok(row.map(|r| User { + id: r.get("id"), + username: r.get("username"), + email: r.get("email"), + password_hash: r.get("password_hash"), + created_at: r.get("created_at"), + })) } -pub async fn get_user_by_username(pool: &SqlitePool, username: &str) -> sqlx::Result> { - sqlx::query_as::<_, User>( - "SELECT id, username, email, password_hash, created_at FROM users WHERE username = ?", - ) - .bind(username) - .fetch_optional(pool) - .await +pub async fn get_user_by_username(pool: &Pool, username: &str) -> Result, DbError> { + let client = pool.get().await?; + let row = client + .query_opt( + "SELECT id, username, email, password_hash, created_at FROM users WHERE username = $1", + &[&username], + ) + .await?; + Ok(row.map(|r| User { + id: r.get("id"), + username: r.get("username"), + email: r.get("email"), + password_hash: r.get("password_hash"), + created_at: r.get("created_at"), + })) } // ── Game records ───────────────────────────────────────────────────────────── /// Creates a new game record when a room opens. Returns the record id. pub async fn insert_game_record( - pool: &SqlitePool, + pool: &Pool, game_id: &str, room_code: &str, -) -> sqlx::Result { - let id = sqlx::query( - "INSERT INTO game_records (game_id, room_code, started_at) VALUES (?, ?, ?)", - ) - .bind(game_id) - .bind(room_code) - .bind(now_unix()) - .execute(pool) - .await? - .last_insert_rowid(); - Ok(id) +) -> Result { + let client = pool.get().await?; + let row = client + .query_one( + "INSERT INTO game_records (game_id, room_code, started_at) \ + VALUES ($1, $2, $3) RETURNING id", + &[&game_id, &room_code, &now_unix()], + ) + .await?; + Ok(row.get(0)) } /// Stamps `ended_at` and stores the opaque result JSON supplied by the game. pub async fn close_game_record( - pool: &SqlitePool, + pool: &Pool, record_id: i64, result_json: Option<&str>, -) -> sqlx::Result<()> { +) -> Result<(), DbError> { // AND ended_at IS NULL prevents overwriting a result already set by POST /games/result - sqlx::query( - "UPDATE game_records SET ended_at = ?, result = ? WHERE id = ? AND ended_at IS NULL", - ) - .bind(now_unix()) - .bind(result_json) - .bind(record_id) - .execute(pool) - .await?; + let client = pool.get().await?; + client + .execute( + "UPDATE game_records SET ended_at = $1, result = $2 \ + WHERE id = $3 AND ended_at IS NULL", + &[&now_unix(), &result_json, &record_id], + ) + .await?; Ok(()) } /// Records a player's participation in a game. `user_id` is `None` for anonymous players. pub async fn insert_participant( - pool: &SqlitePool, + pool: &Pool, record_id: i64, user_id: Option, player_id: u16, outcome: Option<&str>, -) -> sqlx::Result<()> { - sqlx::query( - "INSERT OR IGNORE INTO game_participants (game_record_id, user_id, player_id, outcome) - VALUES (?, ?, ?, ?)", - ) - .bind(record_id) - .bind(user_id) - .bind(player_id as i64) - .bind(outcome) - .execute(pool) - .await?; +) -> Result<(), DbError> { + let client = pool.get().await?; + client + .execute( + "INSERT INTO game_participants (game_record_id, user_id, player_id, outcome) \ + VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING", + &[&record_id, &user_id, &(player_id as i64), &outcome], + ) + .await?; Ok(()) } /// Returns win/loss/draw counts for a user. All values are 0 when the user has no games. -pub async fn get_user_stats(pool: &SqlitePool, user_id: i64) -> sqlx::Result { - sqlx::query_as::<_, UserStats>( - "SELECT - COUNT(*) as total, - COALESCE(SUM(CASE WHEN outcome = 'win' THEN 1 ELSE 0 END), 0) as wins, - COALESCE(SUM(CASE WHEN outcome = 'loss' THEN 1 ELSE 0 END), 0) as losses, - COALESCE(SUM(CASE WHEN outcome = 'draw' THEN 1 ELSE 0 END), 0) as draws - FROM game_participants - WHERE user_id = ?", - ) - .bind(user_id) - .fetch_one(pool) - .await +pub async fn get_user_stats(pool: &Pool, user_id: i64) -> Result { + let client = pool.get().await?; + let row = client + .query_one( + "SELECT + COUNT(*) as total, + COALESCE(SUM(CASE WHEN outcome = 'win' THEN 1 ELSE 0 END), 0::BIGINT) as wins, + COALESCE(SUM(CASE WHEN outcome = 'loss' THEN 1 ELSE 0 END), 0::BIGINT) as losses, + COALESCE(SUM(CASE WHEN outcome = 'draw' THEN 1 ELSE 0 END), 0::BIGINT) as draws + FROM game_participants + WHERE user_id = $1", + &[&user_id], + ) + .await?; + Ok(UserStats { + total: row.get("total"), + wins: row.get("wins"), + losses: row.get("losses"), + draws: row.get("draws"), + }) } /// Returns a paginated list of games a user participated in, newest first. pub async fn get_user_games( - pool: &SqlitePool, + pool: &Pool, user_id: i64, page: i64, per_page: i64, -) -> sqlx::Result> { - sqlx::query_as::<_, GameSummary>( - "SELECT gr.id, gr.game_id, gr.room_code, gr.started_at, gr.ended_at, gr.result, gp.outcome - FROM game_records gr - JOIN game_participants gp ON gp.game_record_id = gr.id - WHERE gp.user_id = ? - ORDER BY gr.started_at DESC - LIMIT ? OFFSET ?", - ) - .bind(user_id) - .bind(per_page) - .bind(page * per_page) - .fetch_all(pool) - .await +) -> Result, DbError> { + let client = pool.get().await?; + let rows = client + .query( + "SELECT gr.id, gr.game_id, gr.room_code, gr.started_at, gr.ended_at, gr.result, gp.outcome + FROM game_records gr + JOIN game_participants gp ON gp.game_record_id = gr.id + WHERE gp.user_id = $1 + ORDER BY gr.started_at DESC + LIMIT $2 OFFSET $3", + &[&user_id, &per_page, &(page * per_page)], + ) + .await?; + Ok(rows + .into_iter() + .map(|r| GameSummary { + id: r.get("id"), + game_id: r.get("game_id"), + room_code: r.get("room_code"), + started_at: r.get("started_at"), + ended_at: r.get("ended_at"), + result: r.get("result"), + outcome: r.get("outcome"), + }) + .collect()) } diff --git a/server/relay-server/src/http.rs b/server/relay-server/src/http.rs index 4960dc7..832f65d 100644 --- a/server/relay-server/src/http.rs +++ b/server/relay-server/src/http.rs @@ -43,7 +43,7 @@ pub fn router() -> Router> { // ── Error type ──────────────────────────────────────────────────────────────── enum AppError { - Database(sqlx::Error), + Database(db::DbError), NotFound, Conflict(&'static str), BadRequest(&'static str), @@ -67,16 +67,12 @@ impl IntoResponse for AppError { } } -impl From for AppError { - fn from(e: sqlx::Error) -> Self { +impl From for AppError { + fn from(e: db::DbError) -> Self { AppError::Database(e) } } -fn is_unique_violation(e: &sqlx::Error) -> bool { - matches!(e, sqlx::Error::Database(db_err) if db_err.message().contains("UNIQUE constraint failed")) -} - // ── Request / response bodies ───────────────────────────────────────────────── #[derive(Deserialize)] @@ -173,7 +169,7 @@ async fn register( let user_id = db::create_user(&state.db, &body.username, &body.email, &hash) .await .map_err(|e| { - if is_unique_violation(&e) { + if e.is_unique_violation() { AppError::Conflict("username or email already taken") } else { AppError::Database(e) @@ -276,17 +272,7 @@ async fn user_games( // ── Game detail (Phase 5) ───────────────────────────────────────────────────── -#[derive(sqlx::FromRow, Serialize)] -struct GameRecordRow { - id: i64, - game_id: String, - room_code: String, - started_at: i64, - ended_at: Option, - result: Option, -} - -#[derive(sqlx::FromRow, Serialize)] +#[derive(Serialize)] struct ParticipantWithUsername { player_id: i64, outcome: Option, @@ -308,33 +294,46 @@ async fn game_detail( Path(id): Path, State(state): State>, ) -> Result { - let record = sqlx::query_as::<_, GameRecordRow>( - "SELECT id, game_id, room_code, started_at, ended_at, result - FROM game_records WHERE id = ?", - ) - .bind(id) - .fetch_optional(&state.db) - .await? - .ok_or(AppError::NotFound)?; + let client = state.db.get().await.map_err(db::DbError::from)?; - let participants = sqlx::query_as::<_, ParticipantWithUsername>( - "SELECT gp.player_id, gp.outcome, u.username - FROM game_participants gp - LEFT JOIN users u ON u.id = gp.user_id - WHERE gp.game_record_id = ? - ORDER BY gp.player_id", - ) - .bind(id) - .fetch_all(&state.db) - .await?; + let record = client + .query_opt( + "SELECT id, game_id, room_code, started_at, ended_at, result + FROM game_records WHERE id = $1", + &[&id], + ) + .await + .map_err(db::DbError::from)? + .ok_or(AppError::NotFound)?; + + let rows = client + .query( + "SELECT gp.player_id, gp.outcome, u.username + FROM game_participants gp + LEFT JOIN users u ON u.id = gp.user_id + WHERE gp.game_record_id = $1 + ORDER BY gp.player_id", + &[&id], + ) + .await + .map_err(db::DbError::from)?; + + let participants = rows + .into_iter() + .map(|r| ParticipantWithUsername { + player_id: r.get("player_id"), + outcome: r.get("outcome"), + username: r.get("username"), + }) + .collect(); Ok(Json(GameDetailResponse { - id: record.id, - game_id: record.game_id, - room_code: record.room_code, - started_at: record.started_at, - ended_at: record.ended_at, - result: record.result, + id: record.get("id"), + game_id: record.get("game_id"), + room_code: record.get("room_code"), + started_at: record.get("started_at"), + ended_at: record.get("ended_at"), + result: record.get("result"), participants, })) } @@ -362,7 +361,7 @@ struct GameResultResponse { /// /// The room code + game ID act as the shared secret (same trust level as WS join). /// `close_game_record` is idempotent (no-op if already closed), and participant -/// inserts use `INSERT OR IGNORE`, so safe retries are supported. +/// inserts use `ON CONFLICT DO NOTHING`, so safe retries are supported. async fn game_result( State(state): State>, Json(body): Json, diff --git a/server/relay-server/src/lobby.rs b/server/relay-server/src/lobby.rs index 1f33f0b..664f809 100644 --- a/server/relay-server/src/lobby.rs +++ b/server/relay-server/src/lobby.rs @@ -6,7 +6,7 @@ use bytes::Bytes; use serde::{Deserialize, Serialize}; -use sqlx::SqlitePool; +use deadpool_postgres::Pool; use std::collections::HashMap; use std::sync::Arc; use tokio::fs; @@ -57,12 +57,12 @@ pub struct AppState { pub rooms: Mutex>, /// Contains a mapping from game name to the maximum amount of players allowed. pub configs: RwLock>, - /// SQLite connection pool — shared across all request handlers. - pub db: SqlitePool, + /// PostgreSQL connection pool — shared across all request handlers. + pub db: Pool, } impl AppState { - pub fn new(db: SqlitePool) -> Self { + pub fn new(db: Pool) -> Self { Self { rooms: Mutex::new(HashMap::new()), configs: RwLock::new(HashMap::new()), diff --git a/server/relay-server/src/main.rs b/server/relay-server/src/main.rs index 344c637..34acdda 100644 --- a/server/relay-server/src/main.rs +++ b/server/relay-server/src/main.rs @@ -29,7 +29,7 @@ use axum::http::{HeaderName, Method}; use tower_http::cors::{AllowOrigin, CorsLayer}; use tower_http::services::{ServeDir, ServeFile}; use tower_sessions::{Expiry, SessionManagerLayer}; -use tower_sessions_sqlx_store::SqliteStore; +use tower_sessions::MemoryStore; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::main] @@ -51,15 +51,11 @@ async fn main() { ) .init(); - let db_path = std::env::var("DATABASE_PATH").unwrap_or_else(|_| "data/relay.db".to_string()); - let pool = db::init_db(&db_path).await; - - let session_store = SqliteStore::new(pool.clone()); - session_store - .migrate() - .await - .expect("Failed to initialize session store"); + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgresql://trictrac:trictrac@127.0.0.1:5432/trictrac".to_string()); + let pool = db::init_db(&database_url).await; + let session_store = MemoryStore::default(); let session_layer = SessionManagerLayer::new(session_store) .with_secure(false) .with_expiry(Expiry::OnInactivity(TimeDuration::days(30)));