diff --git a/.dockerignore b/.dockerignore index fd65f94..951c3ae 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,4 +24,5 @@ Thumbs.db # Don't ignore these - we need them !web/build/client !web/build/server +!web/build/prerendered !web/build/*.js diff --git a/.sqlx/query-42799df09f28f38b73c4a0f90516dc432e7660d679d3ce8eb448cde2dad81608.json b/.sqlx/query-42799df09f28f38b73c4a0f90516dc432e7660d679d3ce8eb448cde2dad81608.json new file mode 100644 index 0000000..c07748e --- /dev/null +++ b/.sqlx/query-42799df09f28f38b73c4a0f90516dc432e7660d679d3ce8eb448cde2dad81608.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 as check", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "check", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "42799df09f28f38b73c4a0f90516dc432e7660d679d3ce8eb448cde2dad81608" +} diff --git a/.sqlx/query-8adc48c833126d2cd690612a83c1637347e8bdfd230bf46c60ceef8fa096391e.json b/.sqlx/query-8adc48c833126d2cd690612a83c1637347e8bdfd230bf46c60ceef8fa096391e.json new file mode 100644 index 0000000..2b4a494 --- /dev/null +++ b/.sqlx/query-8adc48c833126d2cd690612a83c1637347e8bdfd230bf46c60ceef8fa096391e.json @@ -0,0 +1,98 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT \n id, \n slug, \n title, \n description, \n status as \"status: ProjectStatus\", \n github_repo, \n demo_url, \n priority, \n icon, \n last_github_activity, \n created_at, \n updated_at\n FROM projects\n WHERE status != 'hidden'\n ORDER BY priority DESC, created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "status: ProjectStatus", + "type_info": { + "Custom": { + "name": "project_status", + "kind": { + "Enum": [ + "active", + "maintained", + "archived", + "hidden" + ] + } + } + } + }, + { + "ordinal": 5, + "name": "github_repo", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "demo_url", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "icon", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "last_github_activity", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + false, + true, + true, + false, + false + ] + }, + "hash": "8adc48c833126d2cd690612a83c1637347e8bdfd230bf46c60ceef8fa096391e" +} diff --git a/Cargo.lock b/Cargo.lock index 5b8dd78..9229591 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,14 +76,16 @@ dependencies = [ "axum", "clap", "dashmap", + "dotenvy", "futures", "include_dir", "mime_guess", "nu-ansi-term", - "rand", + "rand 0.9.2", "reqwest", "serde", "serde_json", + "sqlx", "time", "tokio", "tokio-util", @@ -92,6 +94,16 @@ dependencies = [ "tracing", "tracing-subscriber", "ulid", + "uuid", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", ] [[package]] @@ -621,6 +633,9 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] [[package]] name = "block-buffer" @@ -637,6 +652,12 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.0" @@ -748,6 +769,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -802,7 +832,7 @@ checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" dependencies = [ "crc", "digest", - "rand", + "rand 0.9.2", "regex", "rustversion", ] @@ -816,6 +846,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -878,6 +917,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.5" @@ -894,6 +944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -909,6 +960,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dunce" version = "1.0.5" @@ -921,10 +978,10 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", + "der 0.6.1", "elliptic-curve", "rfc6979", - "signature", + "signature 1.6.4", ] [[package]] @@ -932,6 +989,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -941,12 +1001,12 @@ checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ "base16ct", "crypto-bigint 0.4.9", - "der", + "der 0.6.1", "digest", "ff", "generic-array", "group", - "pkcs8", + "pkcs8 0.9.0", "rand_core 0.6.4", "sec1", "subtle", @@ -978,6 +1038,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1000,6 +1082,17 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1069,6 +1162,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -1225,6 +1329,15 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -1237,6 +1350,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1246,6 +1368,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "0.2.12" @@ -1622,6 +1753,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1629,6 +1763,33 @@ version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "litemap" version = "0.8.1" @@ -1732,6 +1893,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1747,6 +1924,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1754,6 +1942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1791,6 +1980,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1809,11 +2004,20 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1832,16 +2036,43 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + [[package]] name = "pkcs8" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1905,7 +2136,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.2", "ring", "rustc-hash", "rustls 0.23.35", @@ -1946,16 +2177,37 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -1993,6 +2245,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.2" @@ -2095,6 +2356,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "signature 2.2.0", + "spki 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -2130,6 +2411,7 @@ checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki 0.103.8", "subtle", @@ -2260,9 +2542,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ "base16ct", - "der", + "der 0.6.1", "generic-array", - "pkcs8", + "pkcs8 0.9.0", "subtle", "zeroize", ] @@ -2419,6 +2701,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "slab" version = "0.4.11" @@ -2430,6 +2722,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -2451,6 +2746,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.6.0" @@ -2458,7 +2762,215 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls 0.23.35", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", ] [[package]] @@ -2467,6 +2979,17 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[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" @@ -2663,6 +3186,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -2817,7 +3351,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" dependencies = [ - "rand", + "rand 0.9.2", "serde", "web-time", ] @@ -2828,12 +3362,33 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[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.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "untrusted" version = "0.9.0" @@ -2876,7 +3431,9 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ + "getrandom 0.3.4", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -2886,6 +3443,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2932,6 +3495,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.106" @@ -3032,6 +3601,34 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -3056,6 +3653,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3098,6 +3704,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3137,6 +3758,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3155,6 +3782,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3173,6 +3806,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3203,6 +3842,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3221,6 +3866,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3239,6 +3890,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3257,6 +3914,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index e95c262..967d8cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "api" version = "0.1.0" edition = "2024" +default-run = "api" [dependencies] aws-config = "1.8.12" @@ -9,6 +10,7 @@ aws-sdk-s3 = "1.119.0" axum = "0.8.8" clap = { version = "4.5.54", features = ["derive", "env"] } dashmap = "6.1.0" +dotenvy = "0.15" futures = "0.3.31" include_dir = "0.7.4" mime_guess = "2.0.5" @@ -17,6 +19,7 @@ rand = "0.9.2" reqwest = { version = "0.13.1", default-features = false, features = ["rustls", "charset", "json", "stream"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.148" +sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "time", "migrate"] } time = { version = "0.3.44", features = ["formatting", "macros"] } tokio = { version = "1.49.0", features = ["full"] } tokio-util = { version = "0.7.18", features = ["io"] } @@ -25,3 +28,4 @@ tower-http = { version = "0.6.8", features = ["trace", "cors", "limit"] } tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } ulid = { version = "1", features = ["serde"] } +uuid = { version = "1", features = ["serde", "v4"] } diff --git a/Dockerfile b/Dockerfile index d53fe4e..a6bf930 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,8 +55,13 @@ RUN cargo chef cook --release --recipe-path recipe.json COPY Cargo.toml Cargo.lock ./ COPY src/ ./src/ -# Copy frontend client assets for embedding +# Copy SQLx offline cache and migrations for compile-time macros +COPY .sqlx/ ./.sqlx/ +COPY migrations/ ./migrations/ + +# Copy frontend assets for embedding COPY --from=frontend /build/build/client ./web/build/client +COPY --from=frontend /build/build/prerendered ./web/build/prerendered # Build with real assets RUN cargo build --release && \ diff --git a/Justfile b/Justfile index ab26862..3ed8372 100644 --- a/Justfile +++ b/Justfile @@ -91,3 +91,87 @@ docker-run-json port="8080": docker stop xevion-dev-container 2>/dev/null || true docker rm xevion-dev-container 2>/dev/null || true docker run --name xevion-dev-container -p {{port}}:8080 xevion-dev + +[script("bun")] +seed: + const { spawnSync } = await import("child_process"); + + // Ensure DB is running + const db = spawnSync("just", ["db"], { stdio: "inherit" }); + if (db.status !== 0) process.exit(db.status); + + // Run migrations + const migrate = spawnSync("sqlx", ["migrate", "run"], { stdio: "inherit" }); + if (migrate.status !== 0) process.exit(migrate.status); + + // Seed data + const seed = spawnSync("cargo", ["run", "--bin", "seed"], { stdio: "inherit" }); + if (seed.status !== 0) process.exit(seed.status); + + console.log("✅ Database ready with seed data"); + +[script("bun")] +db cmd="start": + const fs = await import("fs/promises"); + const { spawnSync } = await import("child_process"); + + const NAME = "xevion-postgres"; + const USER = "xevion"; + const PASS = "dev"; + const DB = "xevion"; + const PORT = "5432"; + const ENV_FILE = ".env"; + const CMD = "{{cmd}}"; + + const run = (args) => spawnSync("docker", args, { encoding: "utf8" }); + const getContainer = () => { + const res = run(["ps", "-a", "--filter", `name=^${NAME}$`, "--format", "json"]); + return res.stdout.trim() ? JSON.parse(res.stdout) : null; + }; + + const updateEnv = async () => { + const url = `postgresql://${USER}:${PASS}@localhost:${PORT}/${DB}`; + try { + let content = await fs.readFile(ENV_FILE, "utf8"); + content = content.includes("DATABASE_URL=") + ? content.replace(/DATABASE_URL=.*$/m, `DATABASE_URL=${url}`) + : content.trim() + `\nDATABASE_URL=${url}\n`; + await fs.writeFile(ENV_FILE, content); + } catch { + await fs.writeFile(ENV_FILE, `DATABASE_URL=${url}\n`); + } + }; + + const create = () => { + run(["run", "-d", "--name", NAME, "-e", `POSTGRES_USER=${USER}`, + "-e", `POSTGRES_PASSWORD=${PASS}`, "-e", `POSTGRES_DB=${DB}`, + "-p", `${PORT}:5432`, "postgres:16-alpine"]); + console.log("✅ created"); + }; + + const container = getContainer(); + + if (CMD === "rm") { + if (!container) process.exit(0); + run(["stop", NAME]); + run(["rm", NAME]); + console.log("✅ removed"); + } else if (CMD === "reset") { + if (!container) create(); + else { + run(["exec", NAME, "psql", "-U", USER, "-c", `DROP DATABASE IF EXISTS ${DB}`]); + run(["exec", NAME, "psql", "-U", USER, "-c", `CREATE DATABASE ${DB}`]); + console.log("✅ reset"); + } + await updateEnv(); + } else { + if (!container) { + create(); + } else if (container.State !== "running") { + run(["start", NAME]); + console.log("✅ started"); + } else { + console.log("✅ running"); + } + await updateEnv(); + } diff --git a/migrations/20260106073535_initial_schema.sql b/migrations/20260106073535_initial_schema.sql new file mode 100644 index 0000000..5b70558 --- /dev/null +++ b/migrations/20260106073535_initial_schema.sql @@ -0,0 +1,35 @@ +-- Project status enum +CREATE TYPE project_status AS ENUM ('active', 'maintained', 'archived', 'hidden'); + +-- Projects table +CREATE TABLE projects ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + slug TEXT NOT NULL UNIQUE, + title TEXT NOT NULL, + description TEXT NOT NULL, + status project_status NOT NULL DEFAULT 'active', + github_repo TEXT, + demo_url TEXT, + priority INTEGER NOT NULL DEFAULT 0, + icon TEXT, + last_github_activity TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Indexes for common queries +CREATE INDEX idx_projects_status ON projects(status); +CREATE INDEX idx_projects_priority ON projects(priority DESC); +CREATE INDEX idx_projects_slug ON projects(slug); + +-- Trigger to auto-update updated_at +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_projects_updated_at BEFORE UPDATE ON projects + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); diff --git a/src/bin/seed.rs b/src/bin/seed.rs new file mode 100644 index 0000000..ecf45ef --- /dev/null +++ b/src/bin/seed.rs @@ -0,0 +1,103 @@ +use sqlx::PgPool; + +#[tokio::main] +async fn main() -> Result<(), Box> { + dotenvy::dotenv().ok(); + + let database_url = std::env::var("DATABASE_URL")?; + let pool = PgPool::connect(&database_url).await?; + + println!("🌱 Seeding database..."); + + // Clear existing data + sqlx::query("DELETE FROM projects").execute(&pool).await?; + + // Seed projects with diverse data + let projects = vec![ + ( + "xevion-dev", + "xevion.dev", + "Personal portfolio site with fuzzy tag discovery and ISR caching", + "active", + Some("Xevion/xevion.dev"), + None, + 10, + Some("fa-globe"), + ), + ( + "contest", + "Contest", + "Archive and analysis platform for competitive programming problems", + "active", + Some("Xevion/contest"), + Some("https://contest.xevion.dev"), + 9, + Some("fa-trophy"), + ), + ( + "reforge", + "Reforge", + "Rust library for parsing and manipulating Replay files from Rocket League", + "maintained", + Some("Xevion/reforge"), + None, + 8, + Some("fa-file-code"), + ), + ( + "algorithms", + "Algorithms", + "Collection of algorithm implementations and data structures in Python", + "archived", + Some("Xevion/algorithms"), + None, + 5, + Some("fa-brain"), + ), + ( + "wordplay", + "WordPlay", + "Interactive word game with real-time multiplayer using WebSockets", + "maintained", + Some("Xevion/wordplay"), + Some("https://wordplay.example.com"), + 7, + Some("fa-gamepad"), + ), + ( + "dotfiles", + "Dotfiles", + "Personal configuration files and development environment setup scripts", + "active", + Some("Xevion/dotfiles"), + None, + 6, + Some("fa-terminal"), + ), + ]; + + let project_count = projects.len(); + + for (slug, title, desc, status, repo, demo, priority, icon) in projects { + sqlx::query( + r#" + INSERT INTO projects (slug, title, description, status, github_repo, demo_url, priority, icon) + VALUES ($1, $2, $3, $4::project_status, $5, $6, $7, $8) + "#, + ) + .bind(slug) + .bind(title) + .bind(desc) + .bind(status) + .bind(repo) + .bind(demo) + .bind(priority) + .bind(icon) + .execute(&pool) + .await?; + } + + println!("✅ Seeded {} projects", project_count); + + Ok(()) +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..56eec49 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,123 @@ +use serde::{Deserialize, Serialize}; +use sqlx::{PgPool, postgres::PgPoolOptions}; +use time::OffsetDateTime; +use uuid::Uuid; + +// Database types +#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)] +#[sqlx(type_name = "project_status", rename_all = "lowercase")] +pub enum ProjectStatus { + Active, + Maintained, + Archived, + Hidden, +} + +// Database model +#[derive(Debug, Clone, sqlx::FromRow)] +#[allow(dead_code)] +pub struct DbProject { + pub id: Uuid, + pub slug: String, + pub title: String, + pub description: String, + pub status: ProjectStatus, + pub github_repo: Option, + pub demo_url: Option, + pub priority: i32, + pub icon: Option, + pub last_github_activity: Option, + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, +} + +// API response types +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApiProjectLink { + pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApiProject { + pub id: String, + pub name: String, + #[serde(rename = "shortDescription")] + pub short_description: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + pub links: Vec, +} + +impl DbProject { + /// Convert database project to API response format + pub fn to_api_project(&self) -> ApiProject { + let mut links = Vec::new(); + + if let Some(ref repo) = self.github_repo { + links.push(ApiProjectLink { + url: format!("https://github.com/{}", repo), + title: Some("GitHub".to_string()), + }); + } + + if let Some(ref demo) = self.demo_url { + links.push(ApiProjectLink { + url: demo.clone(), + title: Some("Demo".to_string()), + }); + } + + ApiProject { + id: self.id.to_string(), + name: self.title.clone(), + short_description: self.description.clone(), + icon: self.icon.clone(), + links, + } + } +} + +// Connection pool creation +pub async fn create_pool(database_url: &str) -> Result { + PgPoolOptions::new() + .max_connections(20) + .acquire_timeout(std::time::Duration::from_secs(3)) + .connect(database_url) + .await +} + +// Queries +pub async fn get_public_projects(pool: &PgPool) -> Result, sqlx::Error> { + sqlx::query_as!( + DbProject, + r#" + SELECT + id, + slug, + title, + description, + status as "status: ProjectStatus", + github_repo, + demo_url, + priority, + icon, + last_github_activity, + created_at, + updated_at + FROM projects + WHERE status != 'hidden' + ORDER BY priority DESC, created_at DESC + "# + ) + .fetch_all(pool) + .await +} + +pub async fn health_check(pool: &PgPool) -> Result<(), sqlx::Error> { + sqlx::query!("SELECT 1 as check") + .fetch_one(pool) + .await + .map(|_| ()) +} diff --git a/src/main.rs b/src/main.rs index 538210f..fa07f36 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ use axum::{ routing::any, }; use clap::Parser; -use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; @@ -16,6 +15,7 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitEx mod assets; mod config; +mod db; mod formatter; mod health; mod middleware; @@ -68,9 +68,30 @@ fn init_tracing() { #[tokio::main] async fn main() { + // Load .env file if present + dotenvy::dotenv().ok(); + + // Parse args early to allow --help to work without database + let args = Args::parse(); + init_tracing(); - let args = Args::parse(); + // Load database URL from environment (fail-fast) + let database_url = + std::env::var("DATABASE_URL").expect("DATABASE_URL must be set in environment"); + + // Create connection pool + let pool = db::create_pool(&database_url) + .await + .expect("Failed to connect to database"); + + // Run migrations on startup + sqlx::migrate!() + .run(&pool) + .await + .expect("Failed to run database migrations"); + + tracing::info!("Database connected and migrations applied"); if args.listen.is_empty() { eprintln!("Error: At least one --listen address is required"); @@ -108,13 +129,15 @@ async fn main() { let downstream_url_for_health = args.downstream.clone(); let http_client_for_health = http_client.clone(); let unix_client_for_health = unix_client.clone(); + let pool_for_health = pool.clone(); let health_checker = Arc::new(HealthChecker::new(move || { let downstream_url = downstream_url_for_health.clone(); let http_client = http_client_for_health.clone(); let unix_client = unix_client_for_health.clone(); + let pool = pool_for_health.clone(); - async move { perform_health_check(downstream_url, http_client, unix_client).await } + async move { perform_health_check(downstream_url, http_client, unix_client, Some(pool)).await } })); let tarpit_config = TarpitConfig::from_env(); @@ -137,6 +160,7 @@ async fn main() { unix_client, health_checker, tarpit_state, + pool: pool.clone(), }); // Regenerate common OGP images on startup @@ -238,6 +262,7 @@ pub struct AppState { unix_client: Option, health_checker: Arc, tarpit_state: Arc, + pool: sqlx::PgPool, } #[derive(Debug)] @@ -289,10 +314,7 @@ fn api_routes() -> Router> { "/health", axum::routing::get(health_handler).head(health_handler), ) - .route( - "/projects", - axum::routing::get(projects_handler).head(projects_handler), - ) + .route("/projects", axum::routing::get(projects_handler)) .fallback(api_404_and_method_handler) } @@ -423,55 +445,25 @@ async fn api_404_handler(uri: axum::http::Uri) -> impl IntoResponse { api_404_and_method_handler(req).await } -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ProjectLink { - url: String, - #[serde(skip_serializing_if = "Option::is_none")] - title: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Project { - id: String, - name: String, - #[serde(rename = "shortDescription")] - short_description: String, - #[serde(skip_serializing_if = "Option::is_none")] - icon: Option, - links: Vec, -} - -async fn projects_handler() -> impl IntoResponse { - let projects = vec![ - Project { - id: "1".to_string(), - name: "xevion.dev".to_string(), - short_description: "Personal portfolio with fuzzy tag discovery".to_string(), - icon: None, - links: vec![ProjectLink { - url: "https://github.com/Xevion/xevion.dev".to_string(), - title: Some("GitHub".to_string()), - }], - }, - Project { - id: "2".to_string(), - name: "Contest".to_string(), - short_description: "Competitive programming problem archive".to_string(), - icon: None, - links: vec![ - ProjectLink { - url: "https://github.com/Xevion/contest".to_string(), - title: Some("GitHub".to_string()), - }, - ProjectLink { - url: "https://contest.xevion.dev".to_string(), - title: Some("Demo".to_string()), - }, - ], - }, - ]; - - Json(projects) +async fn projects_handler(State(state): State>) -> impl IntoResponse { + match db::get_public_projects(&state.pool).await { + Ok(projects) => { + let api_projects: Vec = + projects.into_iter().map(|p| p.to_api_project()).collect(); + Json(api_projects).into_response() + } + Err(err) => { + tracing::error!(error = %err, "Failed to fetch projects from database"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "Internal server error", + "message": "Failed to fetch projects" + })), + ) + .into_response() + } + } } fn should_tarpit(state: &TarpitState, path: &str) -> bool { @@ -687,6 +679,7 @@ async fn perform_health_check( downstream_url: String, http_client: reqwest::Client, unix_client: Option, + pool: Option, ) -> bool { let url = if downstream_url.starts_with('/') || downstream_url.starts_with("./") { "http://localhost/internal/health".to_string() @@ -700,24 +693,40 @@ async fn perform_health_check( &http_client }; - match tokio::time::timeout(Duration::from_secs(5), client.get(&url).send()).await { - Ok(Ok(response)) => { - let is_success = response.status().is_success(); - if !is_success { - tracing::warn!( - status = response.status().as_u16(), - "Health check failed: Bun returned non-success status" - ); + let bun_healthy = + match tokio::time::timeout(Duration::from_secs(5), client.get(&url).send()).await { + Ok(Ok(response)) => { + let is_success = response.status().is_success(); + if !is_success { + tracing::warn!( + status = response.status().as_u16(), + "Health check failed: Bun returned non-success status" + ); + } + is_success + } + Ok(Err(err)) => { + tracing::error!(error = %err, "Health check failed: cannot reach Bun"); + false + } + Err(_) => { + tracing::error!("Health check failed: timeout after 5s"); + false + } + }; + + // Check database + let db_healthy = if let Some(pool) = pool { + match db::health_check(&pool).await { + Ok(_) => true, + Err(err) => { + tracing::error!(error = %err, "Database health check failed"); + false } - is_success } - Ok(Err(err)) => { - tracing::error!(error = %err, "Health check failed: cannot reach Bun"); - false - } - Err(_) => { - tracing::error!("Health check failed: timeout after 5s"); - false - } - } + } else { + true + }; + + bun_healthy && db_healthy }