From a8efdac3dd200f559008b938335483cacc2c7c90 Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Sat, 6 Jan 2024 16:54:11 -0800 Subject: [PATCH] add invitation support to the db --- Cargo.lock | 292 ++++++++++++------ Cargo.toml | 1 + julid | 2 +- migrations/20230426221940_init.up.sql | 11 + .../20230427212229_update_triggers.up.sql | 7 + src/bin/mkinvites.rs | 80 +++++ src/import_utils.rs | 6 +- src/lib.rs | 24 +- src/main.rs | 4 +- src/{signup.rs => signup/handlers.rs} | 179 ++++++++--- src/signup/mod.rs | 72 +++++ src/signup/templates.rs | 28 ++ src/templates.rs | 16 - src/test_utils.rs | 34 +- src/users.rs | 28 ++ src/watches/handlers.rs | 16 +- templates/my_watches_page.html | 2 +- templates/signup_error.html | 16 + 18 files changed, 623 insertions(+), 195 deletions(-) create mode 100644 src/bin/mkinvites.rs rename src/{signup.rs => signup/handlers.rs} (77%) create mode 100644 src/signup/mod.rs create mode 100644 src/signup/templates.rs create mode 100644 templates/signup_error.html diff --git a/Cargo.lock b/Cargo.lock index a3e6311..7c40a3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", "getrandom", @@ -110,9 +110,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.76" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "argon2" @@ -147,7 +147,7 @@ checksum = "a41603f7cdbf5ac4af60760f17253eb6adf6ec5b6f14a7ed830cf687d375f163" dependencies = [ "askama", "axum-core", - "http", + "http 1.0.0", ] [[package]] @@ -163,7 +163,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] @@ -183,13 +183,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.75" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] @@ -225,16 +225,16 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" +checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d" dependencies = [ "async-trait", "axum-core", "axum-macros", "bytes", "futures-util", - "http", + "http 1.0.0", "http-body", "http-body-util", "hyper", @@ -255,18 +255,19 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-core" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" +checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df" dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 1.0.0", "http-body", "http-body-util", "mime", @@ -275,13 +276,14 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-login" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f10f9f96befdaed5ba6668b1d428824ef2ddde2a0d8e3f640b8100c486679fa" +checksum = "d18e5b44cbb5815db20bc18b2d178cd4fe5e942a5f1faad026f1cd5e53833f4d" dependencies = [ "async-trait", "axum", @@ -306,14 +308,14 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] name = "axum-test" -version = "14.0.0" +version = "14.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ac99db40006a1a3fffeb381f2a78cb341dbc99d07b561e8bd119e22a2b1b0f" +checksum = "e2d15e9969313df61a64e25ce39cc8e586d42432696a0c8e0cfac1d377013d9c" dependencies = [ "anyhow", "async-trait", @@ -321,12 +323,14 @@ dependencies = [ "axum", "bytes", "cookie", - "http", + "http 1.0.0", "http-body-util", "hyper", "hyper-util", + "mime", "pretty_assertions", "reserve-port", + "rust-multipart-rfc7578_2", "serde", "serde_json", "serde_urlencoded", @@ -353,9 +357,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.5" +version = "0.21.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "c79fed4cdb43e993fcdadc7e58a09fd0e3e649c4436fa11da71c9f1f3ee7feb9" [[package]] name = "base64ct" @@ -365,9 +369,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "basic-toml" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f2139706359229bfa8f19142ac1155b4b80beafb7a60471ac5dd109d4a19778" +checksum = "2db21524cad41c5591204d22d75e1970a2d1f71060214ca931dc7d5afe2c14e5" dependencies = [ "serde", ] @@ -453,9 +457,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.11" +version = "4.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "33e92c5c1a78c62968ec57dbc2440366a2d6e5a23faf829970ff1585dc6b18e2" dependencies = [ "clap_builder", "clap_derive", @@ -463,9 +467,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "f4323769dc8a61e2c39ad7dc26f6f2800524691a44d74fe3d1071a5c24db6370" dependencies = [ "anstream", "anstyle", @@ -484,7 +488,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] @@ -524,9 +528,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -548,22 +552,18 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crossbeam-queue" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -588,9 +588,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", @@ -766,7 +766,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", @@ -829,16 +829,16 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +checksum = "991910e35c615d8cab86b5ab04be67e6ad24d2bf5f4f11fdbbed26da999bbeab" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", + "http 1.0.0", "indexmap", "slab", "tokio", @@ -913,6 +913,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.0.0" @@ -931,7 +942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http", + "http 1.0.0", ] [[package]] @@ -942,7 +953,7 @@ checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" dependencies = [ "bytes", "futures-util", - "http", + "http 1.0.0", "http-body", "pin-project-lite", ] @@ -984,7 +995,7 @@ dependencies = [ "futures-channel", "futures-util", "h2", - "http", + "http 1.0.0", "http-body", "httparse", "httpdate", @@ -1003,7 +1014,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.0.0", "http-body", "hyper", "pin-project-lite", @@ -1016,9 +1027,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1083,9 +1094,9 @@ dependencies = [ [[package]] name = "julid-rs" -version = "1.6.1803" +version = "1.6.180339" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f72d13254374b783994e8f3ad449a1b671b500746a49ff554354b8f216cfcbba" +checksum = "35d2c64cb630d89e4e193437d73b203fcefe40ad97e1b9c5faf247e92471e553" dependencies = [ "chrono", "clap", @@ -1116,9 +1127,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libm" @@ -1187,9 +1198,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "mime" @@ -1264,6 +1275,31 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1281,6 +1317,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1302,6 +1348,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -1342,7 +1400,7 @@ name = "optional_optional_user" version = "0.1.0" dependencies = [ "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] @@ -1374,6 +1432,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "parse_duration" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7037e5e93e0172a5a96874380bf73bc6ecef022e26fa25f2be26864d6b3ba95d" +dependencies = [ + "lazy_static", + "num", + "regex", +] + [[package]] name = "password-auth" version = "1.0.0" @@ -1435,7 +1504,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] @@ -1501,18 +1570,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1602,9 +1671,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reserve-port" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3969e7fe15c6c1532ba1a761628298e870bbd18c252fd41a58445f6091c372a0" +checksum = "9838134a2bfaa8e1f40738fcc972ac799de6e0e06b5157acb95fc2b05a0ea283" dependencies = [ "lazy_static", "thiserror", @@ -1666,6 +1735,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 0.2.11", + "mime", + "mime_guess", + "rand", + "thiserror", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1705,29 +1790,29 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -1736,9 +1821,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" dependencies = [ "itoa", "serde", @@ -1780,9 +1865,9 @@ dependencies = [ [[package]] name = "sha256" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7895c8ae88588ccead14ff438b939b0c569cd619116f14b4d13fdff7b8333386" +checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" dependencies = [ "async-trait", "bytes", @@ -2103,9 +2188,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -2120,9 +2205,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.42" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2137,35 +2222,35 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] @@ -2261,7 +2346,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] @@ -2315,7 +2400,7 @@ dependencies = [ "axum-core", "cookie", "futures-util", - "http", + "http 1.0.0", "parking_lot", "pin-project-lite", "tower-layer", @@ -2331,7 +2416,7 @@ dependencies = [ "bitflags 2.4.1", "bytes", "futures-util", - "http", + "http 1.0.0", "http-body", "http-body-util", "http-range-header", @@ -2379,7 +2464,7 @@ dependencies = [ "async-trait", "axum-core", "futures", - "http", + "http 1.0.0", "parking_lot", "serde", "serde_json", @@ -2439,7 +2524,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] @@ -2640,7 +2725,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -2662,7 +2747,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2687,10 +2772,11 @@ dependencies = [ "axum-test", "chrono", "clap", - "http", + "http 1.0.0", "julid-rs", "justerror", "optional_optional_user", + "parse_duration", "password-auth", "password-hash", "rand", @@ -2737,11 +2823,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -2899,7 +2985,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.48", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3564b70..0ee6181 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ tower-sessions = { version = "0.8", default-features = false, features = ["sqlit tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } unicode-segmentation = "1" +parse_duration = "2.1.1" [dev-dependencies] axum-test = "14" diff --git a/julid b/julid index f2ade6d..705afc1 160000 --- a/julid +++ b/julid @@ -1 +1 @@ -Subproject commit f2ade6d85eddfbcaa54f106564deb6252bfc81df +Subproject commit 705afc19e953133aadf811a0a51597e169f7aa62 diff --git a/migrations/20230426221940_init.up.sql b/migrations/20230426221940_init.up.sql index 77fe487..52a41ba 100644 --- a/migrations/20230426221940_init.up.sql +++ b/migrations/20230426221940_init.up.sql @@ -11,9 +11,20 @@ create table if not exists users ( email text, last_seen int, pwhash blob not null, + invited_by blob not null, last_updated int not null default (unixepoch()) ); +-- invitations +create table if not exists invites ( + id blob not null primary key default (julid_new()), + owner blob not null, + expires_at int, + remaining int not null default 1, + last_updated int not null default (unixepoch()), + foreign key (owner) references users (id) on delete cascade on update no action +); + -- table of things to watch create table if not exists watches ( id blob not null primary key default (julid_new()), diff --git a/migrations/20230427212229_update_triggers.up.sql b/migrations/20230427212229_update_triggers.up.sql index 5cca68a..ce44b1e 100644 --- a/migrations/20230427212229_update_triggers.up.sql +++ b/migrations/20230427212229_update_triggers.up.sql @@ -5,6 +5,13 @@ BEGIN update users set last_updated = (select unixepoch()) where id=NEW.id; END; +create trigger if not exists update_last_updated_invites + after update on invites + when OLD.last_updated = NEW.last_updated or OLD.last_updated is null +BEGIN + update invites set last_updated = (select unixepoch()) where id=NEW.id; +END; + create trigger if not exists insert_user_follows after insert on users BEGIN diff --git a/src/bin/mkinvites.rs b/src/bin/mkinvites.rs new file mode 100644 index 0000000..168b3fc --- /dev/null +++ b/src/bin/mkinvites.rs @@ -0,0 +1,80 @@ +use std::time::Duration; + +use clap::Parser; +use julid::Julid; +use parse_duration::parse; +use sqlx::SqlitePool; +use what2watch::{get_db_pool, Invitation, User}; + +#[derive(Debug, Parser)] +struct Cli { + #[clap(long, short, help = "Expire after period (eg, '5h', '1y', etc.)")] + pub expires_in: Option, + #[clap(long, short, help = "Number of times the invitation can be used")] + pub uses: Option, + #[clap(long, short, help = "ID of the user creating the invite", default_value_t = Julid::omega().as_string())] + pub owner: String, + #[clap(long, short, help = "Number of invites to create", default_value_t = 1)] + pub number: u8, +} + +struct Iq { + owner: Julid, + expires: Option, + uses: Option, +} + +fn main() { + let cli = Cli::parse(); + let num = cli.number; + let owner = Julid::from_string(&cli.owner).expect("Malformed ID given for owner"); + let expires = cli + .expires_in + .map(|e| parse(&e).expect("Could not parse {e} as a duration")); + let uses = cli.uses; + let quest = Iq { + owner, + expires, + uses, + }; + + let pool = get_db_pool(); + + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + let invites = rt.block_on(async { + ensure_omega(&pool).await; + generate_invites(quest, num, &pool).await + }); + for invite in invites { + println!("{invite}"); + } +} + +async fn ensure_omega(pool: &SqlitePool) { + User::omega() + .try_insert(pool) + .await + .expect("Could not ensure Omega"); +} + +async fn generate_invites(quest: Iq, number: u8, pool: &SqlitePool) -> Vec { + let mut invites = Vec::with_capacity(number as usize); + for _ in 0..number { + let mut invite = Invitation::new(quest.owner); + if let Some(uses) = quest.uses { + invite = invite.with_uses(uses); + } + if let Some(expires) = quest.expires { + invite = invite.with_expires_in(expires); + } + let invite = Invitation::commit(&invite, pool) + .await + .expect("Error inserting invite into DB"); + invites.push(invite); + } + invites +} diff --git a/src/import_utils.rs b/src/import_utils.rs index 0121c79..2acd200 100644 --- a/src/import_utils.rs +++ b/src/import_utils.rs @@ -127,7 +127,7 @@ pub async fn add_omega_watches( pub async fn ensure_omega(db_pool: &SqlitePool) -> Julid { if !check_omega_exists(db_pool).await { - sqlx::query("insert into users (id, username, pwhash) values (?, 'the omega user', 'you shall not password')").bind(OMEGA_ID).execute(db_pool).await.unwrap(); + User::omega().try_insert(db_pool).await.unwrap(); } OMEGA_ID } @@ -156,10 +156,12 @@ mod test { let p = crate::db::get_db_pool(); let rt = Runtime::new().unwrap(); rt.block_on(async { + dbg!("checking omega"); assert!(!check_omega_exists(&p).await); + dbg!("no omega"); ensure_omega(&p).await; }); - + dbg!("maybe omega"); assert!(rt.block_on(check_omega_exists(&p))); } } diff --git a/src/lib.rs b/src/lib.rs index 8352bf1..f590323 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,9 @@ -use axum::{error_handling::HandleErrorLayer, routing::IntoMakeService, BoxError}; +use axum::{ + error_handling::HandleErrorLayer, + middleware, + routing::{get, post, IntoMakeService}, + BoxError, +}; use sqlx::SqlitePool; #[macro_use] extern crate justerror; @@ -8,6 +13,7 @@ extern crate justerror; pub use db::get_db_pool; pub mod import_utils; +pub use signup::Invitation; pub use users::User; pub use watches::{ShowKind, Watch, WatchQuest}; @@ -34,10 +40,9 @@ use watches::templates::*; pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService { // don't bother bringing handlers into the whole crate namespace use auth::*; - use axum::{middleware, routing::get}; use generic_handlers::{handle_slash, handle_slash_redir}; use login::{get_login, get_logout, post_login, post_logout}; - use signup::{get_create_user, get_signup_success, post_create_user}; + use signup::handlers::{get_create_user, get_signup_success, post_create_user}; use tower_http::services::ServeDir; use watches::handlers::{ get_add_new_watch, get_search_watch, get_watch, get_watch_status, get_watches, @@ -57,17 +62,20 @@ pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService { let assets_dir = std::env::current_dir().unwrap().join("assets"); let assets_svc = ServeDir::new(assets_dir.as_path()); + tracing_subscriber::fmt().with_target(false).pretty().init(); + axum::Router::new() .route("/", get(handle_slash).post(handle_slash)) .nest_service("/assets", assets_svc) - .route("/signup", get(get_create_user).post(post_create_user)) - .route("/signup_success/:id", get(get_signup_success)) + .route("/signup/:invitation", get(get_create_user)) + .route("/signup/", post(post_create_user)) + .route("/signup_success/:user", get(get_signup_success)) .route("/login", get(get_login).post(post_login)) .route("/logout", get(get_logout).post(post_logout)) .route("/watches", get(get_watches)) .route("/watch", get(get_watch)) - .route("/watch/:id", get(get_watch)) - .route("/watch/status/:id", get(get_watch_status)) + .route("/watch/:watch", get(get_watch)) + .route("/watch/status/:watch", get(get_watch_status)) .route("/search", get(get_search_watch)) .route("/add", get(get_add_new_watch).post(post_add_new_watch)) .route( @@ -92,7 +100,7 @@ pub mod test_utils; //-************************************************************************ #[cfg(test)] mod test { - use super::{MainPage, OptionalOptionalUser, SignupSuccessPage, User}; + use super::{signup::templates::SignupSuccessPage, MainPage, OptionalOptionalUser, User}; #[test] fn main_page_has_optional_user() { diff --git a/src/main.rs b/src/main.rs index 0864318..977f4d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,11 +7,13 @@ fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "what2watch=debug,axum::routing=info".into()), + .unwrap_or_else(|_| "what2watch=debug,axum::routing=debug".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); + //tracing_subscriber::fmt().with_target(false).pretty().init(); + let pool = get_db_pool(); let rt = tokio::runtime::Builder::new_multi_thread() diff --git a/src/signup.rs b/src/signup/handlers.rs similarity index 77% rename from src/signup.rs rename to src/signup/handlers.rs index 869cde1..64a64b2 100644 --- a/src/signup.rs +++ b/src/signup/handlers.rs @@ -9,13 +9,14 @@ use axum::{ }; use julid::Julid; use serde::Deserialize; -use sqlx::{query_as, SqlitePool}; +use sqlx::{query_as, Sqlite, SqlitePool}; use unicode_segmentation::UnicodeSegmentation; -use crate::{util::empty_string_as_none, SignupPage, SignupSuccessPage, User}; +use super::{templates::*, Invitation}; +use crate::{util::empty_string_as_none, User}; pub(crate) const CREATE_QUERY: &str = - "insert into users (username, displayname, email, pwhash) values ($1, $2, $3, $4) returning *"; + "insert into users (username, displayname, email, pwhash, invited_by) values ($1, $2, $3, $4, $5) returning *"; const ID_QUERY: &str = "select * from users where id = $1"; //-************************************************************************ @@ -32,6 +33,11 @@ impl IntoResponse for CreateUserError { CreateUserErrorKind::UnknownDBError => { (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response() } + CreateUserErrorKind::BadInvitation => ( + StatusCode::OK, + SignupErrorPage("Sorry, that invitation isn't valid.".to_string()), + ) + .into_response(), _ => (StatusCode::OK, format!("{self}")).into_response(), } } @@ -40,6 +46,7 @@ impl IntoResponse for CreateUserError { #[Error] #[non_exhaustive] pub enum CreateUserErrorKind { + BadInvitation, AlreadyExists, #[error(desc = "Usernames must be between 1 and 20 characters long")] BadUsername, @@ -61,6 +68,7 @@ pub struct SignupForm { pub email: Option, pub password: String, pub pw_verify: String, + pub invitation: String, } //-************************************************************************ @@ -68,8 +76,19 @@ pub struct SignupForm { //-************************************************************************ /// Get Handler: displays the form to create a user -pub async fn get_create_user() -> SignupPage { - SignupPage::default() +#[axum::debug_handler] +pub async fn get_create_user( + State(_pool): State, + invitation: Option>, +) -> Result { + let invitation = invitation.ok_or(CreateUserErrorKind::BadInvitation)?; + let invitation = + Julid::from_string(&invitation.0).map_err(|_| CreateUserErrorKind::BadInvitation)?; + + Ok(SignupPage { + invitation, + ..Default::default() + }) } /// Post Handler: validates form values and calls the actual, private user @@ -79,6 +98,8 @@ pub async fn post_create_user( State(pool): State, Form(signup): Form, ) -> Result { + dbg!(&signup); + use crate::util::validate_optional_length; let username = signup.username.trim(); let password = signup.password.trim(); @@ -108,7 +129,15 @@ pub async fn post_create_user( let email = validate_optional_length(&signup.email, 5..30, CreateUserErrorKind::BadEmail)?; - let user = create_user(username, &displayname, &email, password, &pool).await?; + let user = create_user( + username, + &displayname, + &email, + password, + &pool, + &signup.invitation, + ) + .await?; let when = user.id.created_at(); tracing::debug!("created {user:?} at {when}"); @@ -122,6 +151,7 @@ pub async fn get_signup_success( Path(id): Path, State(pool): State, ) -> Response { + dbg!(&id); let id = id.trim(); let id = Julid::from_string(id).unwrap_or_default(); let user: User = { @@ -153,6 +183,7 @@ pub(crate) async fn create_user( email: &Option, password: &[u8], pool: &SqlitePool, + invitation: &str, ) -> Result { // Argon2 with default params (Argon2id v19) let argon2 = Argon2::default(); @@ -162,28 +193,83 @@ pub(crate) async fn create_user( .unwrap() // safe to unwrap, we know the salt is valid .to_string(); - let res = sqlx::query_as(CREATE_QUERY) + let mut tx = pool.begin().await.map_err(|e| { + tracing::debug!("db error: {e}"); + CreateUserErrorKind::UnknownDBError + })?; + + let invitation = + Julid::from_string(invitation).map_err(|_| CreateUserErrorKind::BadInvitation)?; + + let invited_by = validate_invitation(invitation, &mut tx).await?; + + let user = sqlx::query_as(CREATE_QUERY) .bind(username) .bind(displayname) .bind(email) .bind(&pwhash) + .bind(invited_by) .fetch_one(pool) - .await; - - Ok(res.map_err(|e| { - match e { - sqlx::Error::Database(db) => { - let exit = db.code().unwrap_or_default().parse().unwrap_or(0); - // https://www.sqlite.org/rescode.html codes for unique constraint violations: - if exit == 2067u32 || exit == 1555 { - CreateUserErrorKind::AlreadyExists - } else { - CreateUserErrorKind::UnknownDBError + .await + .map_err(|e| { + match e { + sqlx::Error::Database(db) => { + let exit = db.code().unwrap_or_default().parse().unwrap_or(0); + // https://www.sqlite.org/rescode.html codes for unique constraint violations: + if exit == 2067u32 || exit == 1555 { + CreateUserErrorKind::AlreadyExists + } else { + CreateUserErrorKind::UnknownDBError + } } + _ => CreateUserErrorKind::UnknownDBError, } - _ => CreateUserErrorKind::UnknownDBError, + })?; + + tx.commit().await.map_err(|e| { + tracing::debug!("db error: {e}"); + CreateUserErrorKind::UnknownDBError + })?; + + Ok(user) +} + +async fn validate_invitation( + invitation: Julid, + tx: &mut sqlx::Transaction<'_, Sqlite>, +) -> Result { + let invitation: Invitation = sqlx::query_as("select * from invites where id = ?") + .bind(invitation) + .fetch_optional(&mut **tx) + .await + .map_err(|e| { + tracing::debug!("db error: {e}"); + CreateUserErrorKind::UnknownDBError + })? + .ok_or(CreateUserErrorKind::BadInvitation)?; + + let remaining = invitation.remaining; + if remaining < 1 { + return Err(CreateUserErrorKind::BadInvitation); + } + let _ = sqlx::query("update invites set remaining = ? where id = ?") + .bind(remaining - 1) + .bind(invitation.id) + .execute(&mut **tx) + .await + .map_err(|e| { + tracing::debug!("db error: {e}"); + CreateUserErrorKind::UnknownDBError + })?; + + if let Some(ts) = invitation.expires_at { + let now = chrono::Utc::now().timestamp(); + if ts < now { + return Err(CreateUserErrorKind::BadInvitation); } - })?) + } + + Ok(invitation.owner) } //-************************************************************************ @@ -193,24 +279,25 @@ pub(crate) async fn create_user( #[cfg(test)] mod test { use axum::http::StatusCode; + use julid::Julid; use tokio::runtime::Runtime; use crate::{ db::get_db_pool, - templates::{SignupPage, SignupSuccessPage}, - test_utils::{massage, server_with_pool, FORM_CONTENT_TYPE}, + signup::templates::{SignupPage, SignupSuccessPage}, + test_utils::{massage, server_with_pool, FORM_CONTENT_TYPE, INVITE_ID_INT}, User, }; - const GOOD_FORM: &str = "username=good_user&displayname=Good+User&password=aaaa&pw_verify=aaaa"; - #[test] fn post_create_user() { let pool = get_db_pool(); let rt = Runtime::new().unwrap(); rt.block_on(async { let server = server_with_pool(&pool).await; - let body = massage(GOOD_FORM); + let id: Julid = INVITE_ID_INT.into(); + let form = format!("username=good_user&displayname=Good+User&password=aaaa&pw_verify=aaaa&invitation={}", id); + let body = massage(&form); let resp = server .post("/signup") @@ -219,11 +306,14 @@ mod test { .content_type(FORM_CONTENT_TYPE) .await; + dbg!(&resp.text()); + assert_eq!(StatusCode::SEE_OTHER, resp.status_code()); // get the new user from the db let user = User::try_get("good_user", &pool).await; assert!(user.is_ok()); + assert!(user.unwrap().is_some()); }); } @@ -233,8 +323,9 @@ mod test { let rt = Runtime::new().unwrap(); rt.block_on(async { let server = server_with_pool(&pool).await; - - let resp = server.get("/signup").await; + let invitation: Julid = INVITE_ID_INT.into(); + let path = format!("/signup/{invitation}"); + let resp = server.get(&path).await; let body = std::str::from_utf8(resp.as_bytes()).unwrap(); let expected = SignupPage::default().to_string(); assert_eq!(&expected, body); @@ -243,11 +334,15 @@ mod test { #[test] fn handle_signup_success() { + dbg!("getting the pool"); let pool = get_db_pool(); + dbg!("got the pool"); let rt = Runtime::new().unwrap(); rt.block_on(async { let server = server_with_pool(&pool).await; - let body = massage(GOOD_FORM); + let id: Julid = INVITE_ID_INT.into(); + let form = format!("username=good_user&displayname=Good+User&password=aaaa&pw_verify=aaaa&invitation={}", id); + let body = massage(&form); let resp = server .post("/signup") @@ -259,9 +354,12 @@ mod test { assert_eq!(StatusCode::SEE_OTHER, resp.status_code()); // get the new user from the db - let user = User::try_get("good_user", &pool).await.unwrap().unwrap(); + let user = User::try_get("good_user", &pool).await.unwrap(); + dbg!(&user); + let user = user.unwrap(); let id = user.id; + let path = format!("/signup_success/{id}"); let resp = server.get(&path).expect_success().await; @@ -276,19 +374,19 @@ mod test { //-************************************************************************ mod failure { use super::*; - use crate::signup::{CreateUserError, CreateUserErrorKind}; + use crate::signup::handlers::{CreateUserError, CreateUserErrorKind}; // various ways to fuck up signup const PASSWORD_MISMATCH_FORM: &str = - "username=bad_user&displayname=Bad+User&password=aaaa&pw_verify=bbbb"; + "username=bad_user&displayname=Bad+User&password=aaaa&pw_verify=bbbb&invitation=0000000000000000000000001A"; const PASSWORD_SHORT_FORM: &str = - "username=bad_user&displayname=Bad+User&password=a&pw_verify=a"; - const PASSWORD_LONG_FORM: &str = "username=bad_user&displayname=Bad+User&password=sphinx+of+black+qwartz+judge+my+vow+etc+etc+yadd+yadda&pw_verify=sphinx+of+black+qwartz+judge+my+vow+etc+etc+yadd+yadda"; + "username=bad_user&displayname=Bad+User&password=a&pw_verify=a&invitation=0000000000000000000000001A"; + const PASSWORD_LONG_FORM: &str = "username=bad_user&displayname=Bad+User&password=sphinx+of+black+qwartz+judge+my+vow+etc+etc+yadd+yadda&pw_verify=sphinx+of+black+qwartz+judge+my+vow+etc+etc+yadd+yadda&invitation=0000000000000000000000001A"; const USERNAME_SHORT_FORM: &str = - "username=&displayname=Bad+User&password=aaaa&pw_verify=aaaa"; + "username=&displayname=Bad+User&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; const USERNAME_LONG_FORM: &str = - "username=bad_user12345678901234567890&displayname=Bad+User&password=aaaa&pw_verify=aaaa"; - const DISPLAYNAME_LONG_FORM: &str = "username=bad_user&displayname=Since+time+immemorial%2C+display+names+have+been+subject+to+a+number+of+conventions%2C+restrictions%2C+usages%2C+and+even+incentives.+Have+we+finally+gone+too+far%3F+In+this+essay%2C+&password=aaaa&pw_verify=aaaa"; + "username=bad_user12345678901234567890&displayname=Bad+User&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; + const DISPLAYNAME_LONG_FORM: &str = "username=bad_user&displayname=Since+time+immemorial%2C+display+names+have+been+subject+to+a+number+of+conventions%2C+restrictions%2C+usages%2C+and+even+incentives.+Have+we+finally+gone+too+far%3F+In+this+essay%2C+&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; #[test] fn password_mismatch() { @@ -301,7 +399,7 @@ mod test { let resp = server .post("/signup") // failure to sign up is not failure to submit the request - .expect_success() + //.expect_success() .bytes(body) .content_type(FORM_CONTENT_TYPE) .await; @@ -381,7 +479,7 @@ mod test { rt.block_on(async { let server = server_with_pool(&pool).await; let form = - format!("username=bad_user&displayname=Test+User&password={pw}&pw_verify={pw}"); + format!("username=bad_user&displayname=Test+User&password={pw}&pw_verify={pw}&invitation=0"); let body = massage(&form); let resp = server @@ -460,7 +558,10 @@ mod test { let rt = Runtime::new().unwrap(); rt.block_on(async { let server = server_with_pool(&pool).await; - let body = massage(GOOD_FORM); + let id: Julid = INVITE_ID_INT.into(); + let form = format!("username=good_user&displayname=Good+User&password=aaaa&pw_verify=aaaa&invitation={}", id); + let body = massage(&form); + //let body = massage(GOOD_FORM); let _resp = server .post("/signup") diff --git a/src/signup/mod.rs b/src/signup/mod.rs new file mode 100644 index 0000000..078d9f2 --- /dev/null +++ b/src/signup/mod.rs @@ -0,0 +1,72 @@ +use std::time::Duration; + +use julid::Julid; +use serde::{Deserialize, Serialize}; +use sqlx::SqlitePool; + +pub mod handlers; +pub mod templates; + +#[Error(desc = "Could not create user.")] +#[non_exhaustive] +pub struct CreateInviteError(#[from] CreateInviteErrorKind); + +#[Error] +#[non_exhaustive] +pub enum CreateInviteErrorKind { + DBError, + TooManyUses, +} + +#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +pub struct Invitation { + pub id: Julid, + pub owner: Julid, + pub expires_at: Option, + remaining: i16, +} + +impl Invitation { + pub async fn commit(&self, db: &SqlitePool) -> Result { + sqlx::query_scalar( + "insert into invites (owner, expires_at, remaining) values (?, ?, ?) returning id", + ) + .bind(self.owner) + .bind(self.expires_at) + .bind(self.remaining) + .fetch_optional(db) + .await + .map_err(|e| { + tracing::debug!("Got error creating invite: {e}"); + CreateInviteErrorKind::DBError + })? + .ok_or(CreateInviteErrorKind::DBError.into()) + } + + pub fn new(owner: Julid) -> Self { + Self { + id: Julid::alpha(), // stand-in value, will let the db fill it in + owner, + expires_at: None, + remaining: 1, + } + } + + pub fn with_uses(&self, uses: u8) -> Self { + Self { + id: self.id, + owner: self.owner, + expires_at: self.expires_at, + remaining: uses as i16, + } + } + + pub fn with_expires_in(&self, expires_in: Duration) -> Self { + Self { + id: self.id, + owner: self.owner, + expires_at: Some((chrono::Utc::now() + expires_in).timestamp()), + remaining: self.remaining, + } + } +} diff --git a/src/signup/templates.rs b/src/signup/templates.rs new file mode 100644 index 0000000..72ab011 --- /dev/null +++ b/src/signup/templates.rs @@ -0,0 +1,28 @@ +use askama::Template; +use julid::Julid; +use serde::{Deserialize, Serialize}; + +use crate::{OptionalOptionalUser, User}; + +#[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)] +#[template(path = "signup.html")] +pub struct SignupPage { + pub username: String, + pub displayname: Option, + pub email: Option, + pub password: String, + pub pw_verify: String, + pub invitation: Julid, +} + +#[derive( + Debug, Clone, Template, Default, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser, +)] +#[template(path = "signup_success.html")] +pub struct SignupSuccessPage(pub User); + +#[derive( + Debug, Clone, Template, Default, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser, +)] +#[template(path = "signup_error.html")] +pub struct SignupErrorPage(pub String); diff --git a/src/templates.rs b/src/templates.rs index 80e3df3..154723b 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -3,22 +3,6 @@ use serde::{Deserialize, Serialize}; use crate::{OptionalOptionalUser, User}; -#[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)] -#[template(path = "signup.html")] -pub struct SignupPage { - pub username: String, - pub displayname: Option, - pub email: Option, - pub password: String, - pub pw_verify: String, -} - -#[derive( - Debug, Clone, Template, Default, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser, -)] -#[template(path = "signup_success.html")] -pub struct SignupSuccessPage(pub User); - #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)] #[template(path = "login_page.html")] pub struct LoginPage { diff --git a/src/test_utils.rs b/src/test_utils.rs index c17cf21..fe17817 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -7,10 +7,16 @@ use crate::User; pub const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded"; -pub async fn server_with_pool(pool: &SqlitePool) -> TestServer { - let user = get_test_user(); +pub const INVITE_ID_INT: u128 = 42; + +pub async fn server_with_pool(pool: &SqlitePool) -> TestServer { + //User::omega().try_insert(pool).await.unwrap(); + + let user = get_test_user(); + user.try_insert(pool).await.unwrap(); + + add_test_invite(pool).await; - insert_user_with_id(&user, pool).await; let r: i32 = sqlx::query_scalar("select count(*) from users") .fetch_one(pool) .await @@ -33,22 +39,20 @@ fn get_test_user() -> User { pwhash: "$argon2id$v=19$m=19456,t=2,p=1$GWsCH1w5RYaP9WWmq+xw0g$hmOEqC+MU+vnEk3bOdkoE+z01mOmmOeX08XyPyjqua8".to_string(), id: Julid::omega(), displayname: Some("Test User".to_string()), + invited_by: Julid::omega(), ..Default::default() } } -async fn insert_user_with_id(user: &User, pool: &SqlitePool) { - sqlx::query( - "insert into users (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)", - ) - .bind(user.id) - .bind(&user.username) - .bind(&user.displayname) - .bind(&user.email) - .bind(&user.pwhash) - .execute(pool) - .await - .unwrap(); +async fn add_test_invite(pool: &SqlitePool) { + let id: Julid = INVITE_ID_INT.into(); + sqlx::query("insert into invites (id, owner, remaining) values (?, ?, ?)") + .bind(id) + .bind(Julid::omega()) + .bind(2) + .execute(pool) + .await + .unwrap(); } // https://www.youtube.com/watch?v=29MJySO7PGg diff --git a/src/users.rs b/src/users.rs index d53a479..9975169 100644 --- a/src/users.rs +++ b/src/users.rs @@ -13,6 +13,8 @@ use crate::AuthSession; const USERNAME_QUERY: &str = "select * from users where username = $1"; const LAST_SEEN_QUERY: &str = "update users set last_seen = (select unixepoch()) where id = $1"; +const INSERT_QUERY: &str = + "insert into users (id, username, displayname, email, pwhash, invited_by) values ($1, $2, $3, $4, $5, $6)"; #[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct User { @@ -22,6 +24,7 @@ pub struct User { pub email: Option, pub last_seen: Option, pub pwhash: String, + pub invited_by: Julid, pub digest: String, } @@ -35,6 +38,7 @@ impl sqlx::FromRow<'_, SqliteRow> for User { displayname: row.try_get("displayname")?, email: row.try_get("email")?, last_seen: row.try_get("last_seen")?, + invited_by: row.try_get("invited_by")?, pwhash, digest, }) @@ -50,6 +54,7 @@ impl Debug for User { .field("email", &self.email) .field("last_seen", &self.last_seen) .field("digest", &self.digest) + .field("invited_by", &self.invited_by.as_string()) .finish() } } @@ -75,6 +80,19 @@ impl User { .await } + pub async fn try_insert(&self, db: &SqlitePool) -> Result<(), sqlx::Error> { + sqlx::query(INSERT_QUERY) + .bind(self.id) + .bind(&self.username) + .bind(&self.displayname) + .bind(&self.email) + .bind(&self.pwhash) + .bind(self.invited_by) + .execute(db) + .await + .map(|_| ()) + } + pub async fn update_last_seen(&self, pool: &SqlitePool) { match sqlx::query(LAST_SEEN_QUERY) .bind(self.id) @@ -88,6 +106,16 @@ impl User { } } } + + pub fn omega() -> Self { + User { + id: Julid::omega(), + username: "omega".into(), + displayname: Some("The One Who Is".into()), + invited_by: Julid::omega(), + ..Default::default() + } + } } //-************************************************************************ diff --git a/src/watches/handlers.rs b/src/watches/handlers.rs index b265ed0..5aad709 100644 --- a/src/watches/handlers.rs +++ b/src/watches/handlers.rs @@ -249,16 +249,14 @@ pub async fn get_search_watch( let query = if search_query == SearchQuery::default() { query_as(DEFAULT_WATCHES_QUERY) + } else if let Some(title) = search_query.title { + let q = format!("%{title}%"); + query_as("select * from watches where title like ?").bind(q) + } else if let Some(search) = search_query.search { + let q = format!("%{search}"); + query_as("select * from watches where title like ?").bind(q) } else { - if let Some(title) = search_query.title { - let q = format!("%{title}%"); - query_as("select * from watches where title like ?").bind(q) - } else if let Some(search) = search_query.search { - let q = format!("%{search}"); - query_as("select * from watches where title like ?").bind(q) - } else { - query_as(DEFAULT_WATCHES_QUERY) - } + query_as(DEFAULT_WATCHES_QUERY) }; // until tantivy search diff --git a/templates/my_watches_page.html b/templates/my_watches_page.html index 0ed8428..aa42ec6 100644 --- a/templates/my_watches_page.html +++ b/templates/my_watches_page.html @@ -28,7 +28,7 @@
    {% for watch in watches %} -
  • {{watch.title}} -- {% call m::get_or_default(watch.year(), "when??") %}: +
  • {{watch.title}} -- {% call m::get_or_default(watch.year(), "when??") %}
  • {% endfor %}
diff --git a/templates/signup_error.html b/templates/signup_error.html new file mode 100644 index 0000000..610bd3f --- /dev/null +++ b/templates/signup_error.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}Dang, Bish{% endblock %} + +{% block content %} +{% block header %}{% endblock %} + +

Oh dang!

+ +
+

+ Sorry, something went wrong: {{self.0}} +

+
+ +{% endblock %}