add invitation support to the db

This commit is contained in:
Joe Ardent 2024-01-06 16:54:11 -08:00
parent e05d74d143
commit a8efdac3dd
18 changed files with 623 additions and 195 deletions

292
Cargo.lock generated
View File

@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.6" version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"getrandom", "getrandom",
@ -110,9 +110,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.76" version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]] [[package]]
name = "argon2" name = "argon2"
@ -147,7 +147,7 @@ checksum = "a41603f7cdbf5ac4af60760f17253eb6adf6ec5b6f14a7ed830cf687d375f163"
dependencies = [ dependencies = [
"askama", "askama",
"axum-core", "axum-core",
"http", "http 1.0.0",
] ]
[[package]] [[package]]
@ -163,7 +163,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -183,13 +183,13 @@ dependencies = [
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.75" version = "0.1.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -225,16 +225,16 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.7.2" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
"axum-macros", "axum-macros",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http 1.0.0",
"http-body", "http-body",
"http-body-util", "http-body-util",
"hyper", "hyper",
@ -255,18 +255,19 @@ dependencies = [
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
name = "axum-core" name = "axum-core"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http 1.0.0",
"http-body", "http-body",
"http-body-util", "http-body-util",
"mime", "mime",
@ -275,13 +276,14 @@ dependencies = [
"sync_wrapper", "sync_wrapper",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
name = "axum-login" name = "axum-login"
version = "0.11.2" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f10f9f96befdaed5ba6668b1d428824ef2ddde2a0d8e3f640b8100c486679fa" checksum = "d18e5b44cbb5815db20bc18b2d178cd4fe5e942a5f1faad026f1cd5e53833f4d"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@ -306,14 +308,14 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
name = "axum-test" name = "axum-test"
version = "14.0.0" version = "14.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56ac99db40006a1a3fffeb381f2a78cb341dbc99d07b561e8bd119e22a2b1b0f" checksum = "e2d15e9969313df61a64e25ce39cc8e586d42432696a0c8e0cfac1d377013d9c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -321,12 +323,14 @@ dependencies = [
"axum", "axum",
"bytes", "bytes",
"cookie", "cookie",
"http", "http 1.0.0",
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-util", "hyper-util",
"mime",
"pretty_assertions", "pretty_assertions",
"reserve-port", "reserve-port",
"rust-multipart-rfc7578_2",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
@ -353,9 +357,9 @@ dependencies = [
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.5" version = "0.21.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" checksum = "c79fed4cdb43e993fcdadc7e58a09fd0e3e649c4436fa11da71c9f1f3ee7feb9"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
@ -365,9 +369,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "basic-toml" name = "basic-toml"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f2139706359229bfa8f19142ac1155b4b80beafb7a60471ac5dd109d4a19778" checksum = "2db21524cad41c5591204d22d75e1970a2d1f71060214ca931dc7d5afe2c14e5"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -453,9 +457,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.4.11" version = "4.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" checksum = "33e92c5c1a78c62968ec57dbc2440366a2d6e5a23faf829970ff1585dc6b18e2"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -463,9 +467,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.4.11" version = "4.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" checksum = "f4323769dc8a61e2c39ad7dc26f6f2800524691a44d74fe3d1071a5c24db6370"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -484,7 +488,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -524,9 +528,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.11" version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -548,22 +552,18 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]] [[package]]
name = "crossbeam-queue" name = "crossbeam-queue"
version = "0.3.10" version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
dependencies = [ dependencies = [
"cfg-if",
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.18" version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
@ -588,9 +588,9 @@ dependencies = [
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.10" version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
"serde", "serde",
@ -766,7 +766,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -810,9 +810,9 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.11" version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@ -829,16 +829,16 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" checksum = "991910e35c615d8cab86b5ab04be67e6ad24d2bf5f4f11fdbbed26da999bbeab"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http 1.0.0",
"indexmap", "indexmap",
"slab", "slab",
"tokio", "tokio",
@ -913,6 +913,17 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "http" name = "http"
version = "1.0.0" version = "1.0.0"
@ -931,7 +942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http 1.0.0",
] ]
[[package]] [[package]]
@ -942,7 +953,7 @@ checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http 1.0.0",
"http-body", "http-body",
"pin-project-lite", "pin-project-lite",
] ]
@ -984,7 +995,7 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"h2", "h2",
"http", "http 1.0.0",
"http-body", "http-body",
"httparse", "httparse",
"httpdate", "httpdate",
@ -1003,7 +1014,7 @@ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"http", "http 1.0.0",
"http-body", "http-body",
"hyper", "hyper",
"pin-project-lite", "pin-project-lite",
@ -1016,9 +1027,9 @@ dependencies = [
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.58" version = "0.1.59"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys",
@ -1083,9 +1094,9 @@ dependencies = [
[[package]] [[package]]
name = "julid-rs" name = "julid-rs"
version = "1.6.1803" version = "1.6.180339"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f72d13254374b783994e8f3ad449a1b671b500746a49ff554354b8f216cfcbba" checksum = "35d2c64cb630d89e4e193437d73b203fcefe40ad97e1b9c5faf247e92471e553"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@ -1116,9 +1127,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.151" version = "0.2.152"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]] [[package]]
name = "libm" name = "libm"
@ -1187,9 +1198,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.6.4" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]] [[package]]
name = "mime" name = "mime"
@ -1264,6 +1275,31 @@ dependencies = [
"winapi", "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]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.4" version = "0.8.4"
@ -1281,6 +1317,16 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -1302,6 +1348,18 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.17" version = "0.2.17"
@ -1342,7 +1400,7 @@ name = "optional_optional_user"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1374,6 +1432,17 @@ dependencies = [
"windows-targets 0.48.5", "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]] [[package]]
name = "password-auth" name = "password-auth"
version = "1.0.0" version = "1.0.0"
@ -1435,7 +1504,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1501,18 +1570,18 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.71" version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.33" version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -1602,9 +1671,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]] [[package]]
name = "reserve-port" name = "reserve-port"
version = "2.0.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3969e7fe15c6c1532ba1a761628298e870bbd18c252fd41a58445f6091c372a0" checksum = "9838134a2bfaa8e1f40738fcc972ac799de6e0e06b5157acb95fc2b05a0ea283"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"thiserror", "thiserror",
@ -1666,6 +1735,22 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.23" version = "0.1.23"
@ -1705,29 +1790,29 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.193" version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.193" version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.108" version = "1.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1736,9 +1821,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_path_to_error" name = "serde_path_to_error"
version = "0.1.14" version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c"
dependencies = [ dependencies = [
"itoa", "itoa",
"serde", "serde",
@ -1780,9 +1865,9 @@ dependencies = [
[[package]] [[package]]
name = "sha256" name = "sha256"
version = "1.4.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7895c8ae88588ccead14ff438b939b0c569cd619116f14b4d13fdff7b8333386" checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@ -2103,9 +2188,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.4.1" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]] [[package]]
name = "syn" name = "syn"
@ -2120,9 +2205,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.42" version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2137,35 +2222,35 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.8.1" version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
"redox_syscall", "redox_syscall",
"rustix", "rustix",
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.51" version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.51" version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2261,7 +2346,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2315,7 +2400,7 @@ dependencies = [
"axum-core", "axum-core",
"cookie", "cookie",
"futures-util", "futures-util",
"http", "http 1.0.0",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"tower-layer", "tower-layer",
@ -2331,7 +2416,7 @@ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http 1.0.0",
"http-body", "http-body",
"http-body-util", "http-body-util",
"http-range-header", "http-range-header",
@ -2379,7 +2464,7 @@ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
"futures", "futures",
"http", "http 1.0.0",
"parking_lot", "parking_lot",
"serde", "serde",
"serde_json", "serde_json",
@ -2439,7 +2524,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2640,7 +2725,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2662,7 +2747,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2687,10 +2772,11 @@ dependencies = [
"axum-test", "axum-test",
"chrono", "chrono",
"clap", "clap",
"http", "http 1.0.0",
"julid-rs", "julid-rs",
"justerror", "justerror",
"optional_optional_user", "optional_optional_user",
"parse_duration",
"password-auth", "password-auth",
"password-hash", "password-hash",
"rand", "rand",
@ -2737,11 +2823,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.51.1" version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.52.0",
] ]
[[package]] [[package]]
@ -2899,7 +2985,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.42", "syn 2.0.48",
] ]
[[package]] [[package]]

View File

@ -35,6 +35,7 @@ tower-sessions = { version = "0.8", default-features = false, features = ["sqlit
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
unicode-segmentation = "1" unicode-segmentation = "1"
parse_duration = "2.1.1"
[dev-dependencies] [dev-dependencies]
axum-test = "14" axum-test = "14"

2
julid

@ -1 +1 @@
Subproject commit f2ade6d85eddfbcaa54f106564deb6252bfc81df Subproject commit 705afc19e953133aadf811a0a51597e169f7aa62

View File

@ -11,9 +11,20 @@ create table if not exists users (
email text, email text,
last_seen int, last_seen int,
pwhash blob not null, pwhash blob not null,
invited_by blob not null,
last_updated int not null default (unixepoch()) 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 -- table of things to watch
create table if not exists watches ( create table if not exists watches (
id blob not null primary key default (julid_new()), id blob not null primary key default (julid_new()),

View File

@ -5,6 +5,13 @@ BEGIN
update users set last_updated = (select unixepoch()) where id=NEW.id; update users set last_updated = (select unixepoch()) where id=NEW.id;
END; 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 create trigger if not exists insert_user_follows
after insert on users after insert on users
BEGIN BEGIN

80
src/bin/mkinvites.rs Normal file
View File

@ -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<String>,
#[clap(long, short, help = "Number of times the invitation can be used")]
pub uses: Option<u8>,
#[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<Duration>,
uses: Option<u8>,
}
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<Julid> {
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
}

View File

@ -127,7 +127,7 @@ pub async fn add_omega_watches(
pub async fn ensure_omega(db_pool: &SqlitePool) -> Julid { pub async fn ensure_omega(db_pool: &SqlitePool) -> Julid {
if !check_omega_exists(db_pool).await { 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 OMEGA_ID
} }
@ -156,10 +156,12 @@ mod test {
let p = crate::db::get_db_pool(); let p = crate::db::get_db_pool();
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
dbg!("checking omega");
assert!(!check_omega_exists(&p).await); assert!(!check_omega_exists(&p).await);
dbg!("no omega");
ensure_omega(&p).await; ensure_omega(&p).await;
}); });
dbg!("maybe omega");
assert!(rt.block_on(check_omega_exists(&p))); assert!(rt.block_on(check_omega_exists(&p)));
} }
} }

View File

@ -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; use sqlx::SqlitePool;
#[macro_use] #[macro_use]
extern crate justerror; extern crate justerror;
@ -8,6 +13,7 @@ extern crate justerror;
pub use db::get_db_pool; pub use db::get_db_pool;
pub mod import_utils; pub mod import_utils;
pub use signup::Invitation;
pub use users::User; pub use users::User;
pub use watches::{ShowKind, Watch, WatchQuest}; pub use watches::{ShowKind, Watch, WatchQuest};
@ -34,10 +40,9 @@ use watches::templates::*;
pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService<axum::Router> { pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService<axum::Router> {
// don't bother bringing handlers into the whole crate namespace // don't bother bringing handlers into the whole crate namespace
use auth::*; use auth::*;
use axum::{middleware, routing::get};
use generic_handlers::{handle_slash, handle_slash_redir}; use generic_handlers::{handle_slash, handle_slash_redir};
use login::{get_login, get_logout, post_login, post_logout}; 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 tower_http::services::ServeDir;
use watches::handlers::{ use watches::handlers::{
get_add_new_watch, get_search_watch, get_watch, get_watch_status, get_watches, 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<axum::Router> {
let assets_dir = std::env::current_dir().unwrap().join("assets"); let assets_dir = std::env::current_dir().unwrap().join("assets");
let assets_svc = ServeDir::new(assets_dir.as_path()); let assets_svc = ServeDir::new(assets_dir.as_path());
tracing_subscriber::fmt().with_target(false).pretty().init();
axum::Router::new() axum::Router::new()
.route("/", get(handle_slash).post(handle_slash)) .route("/", get(handle_slash).post(handle_slash))
.nest_service("/assets", assets_svc) .nest_service("/assets", assets_svc)
.route("/signup", get(get_create_user).post(post_create_user)) .route("/signup/:invitation", get(get_create_user))
.route("/signup_success/:id", get(get_signup_success)) .route("/signup/", post(post_create_user))
.route("/signup_success/:user", get(get_signup_success))
.route("/login", get(get_login).post(post_login)) .route("/login", get(get_login).post(post_login))
.route("/logout", get(get_logout).post(post_logout)) .route("/logout", get(get_logout).post(post_logout))
.route("/watches", get(get_watches)) .route("/watches", get(get_watches))
.route("/watch", get(get_watch)) .route("/watch", get(get_watch))
.route("/watch/:id", get(get_watch)) .route("/watch/:watch", get(get_watch))
.route("/watch/status/:id", get(get_watch_status)) .route("/watch/status/:watch", get(get_watch_status))
.route("/search", get(get_search_watch)) .route("/search", get(get_search_watch))
.route("/add", get(get_add_new_watch).post(post_add_new_watch)) .route("/add", get(get_add_new_watch).post(post_add_new_watch))
.route( .route(
@ -92,7 +100,7 @@ pub mod test_utils;
//-************************************************************************ //-************************************************************************
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{MainPage, OptionalOptionalUser, SignupSuccessPage, User}; use super::{signup::templates::SignupSuccessPage, MainPage, OptionalOptionalUser, User};
#[test] #[test]
fn main_page_has_optional_user() { fn main_page_has_optional_user() {

View File

@ -7,11 +7,13 @@ fn main() {
tracing_subscriber::registry() tracing_subscriber::registry()
.with( .with(
tracing_subscriber::EnvFilter::try_from_default_env() 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()) .with(tracing_subscriber::fmt::layer())
.init(); .init();
//tracing_subscriber::fmt().with_target(false).pretty().init();
let pool = get_db_pool(); let pool = get_db_pool();
let rt = tokio::runtime::Builder::new_multi_thread() let rt = tokio::runtime::Builder::new_multi_thread()

View File

@ -9,13 +9,14 @@ use axum::{
}; };
use julid::Julid; use julid::Julid;
use serde::Deserialize; use serde::Deserialize;
use sqlx::{query_as, SqlitePool}; use sqlx::{query_as, Sqlite, SqlitePool};
use unicode_segmentation::UnicodeSegmentation; 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 = 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"; const ID_QUERY: &str = "select * from users where id = $1";
//-************************************************************************ //-************************************************************************
@ -32,6 +33,11 @@ impl IntoResponse for CreateUserError {
CreateUserErrorKind::UnknownDBError => { CreateUserErrorKind::UnknownDBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response() (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(), _ => (StatusCode::OK, format!("{self}")).into_response(),
} }
} }
@ -40,6 +46,7 @@ impl IntoResponse for CreateUserError {
#[Error] #[Error]
#[non_exhaustive] #[non_exhaustive]
pub enum CreateUserErrorKind { pub enum CreateUserErrorKind {
BadInvitation,
AlreadyExists, AlreadyExists,
#[error(desc = "Usernames must be between 1 and 20 characters long")] #[error(desc = "Usernames must be between 1 and 20 characters long")]
BadUsername, BadUsername,
@ -61,6 +68,7 @@ pub struct SignupForm {
pub email: Option<String>, pub email: Option<String>,
pub password: String, pub password: String,
pub pw_verify: String, pub pw_verify: String,
pub invitation: String,
} }
//-************************************************************************ //-************************************************************************
@ -68,8 +76,19 @@ pub struct SignupForm {
//-************************************************************************ //-************************************************************************
/// Get Handler: displays the form to create a user /// Get Handler: displays the form to create a user
pub async fn get_create_user() -> SignupPage { #[axum::debug_handler]
SignupPage::default() pub async fn get_create_user(
State(_pool): State<SqlitePool>,
invitation: Option<Path<String>>,
) -> Result<impl IntoResponse, CreateUserError> {
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 /// Post Handler: validates form values and calls the actual, private user
@ -79,6 +98,8 @@ pub async fn post_create_user(
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
Form(signup): Form<SignupForm>, Form(signup): Form<SignupForm>,
) -> Result<impl IntoResponse, CreateUserError> { ) -> Result<impl IntoResponse, CreateUserError> {
dbg!(&signup);
use crate::util::validate_optional_length; use crate::util::validate_optional_length;
let username = signup.username.trim(); let username = signup.username.trim();
let password = signup.password.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 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(); let when = user.id.created_at();
tracing::debug!("created {user:?} at {when}"); tracing::debug!("created {user:?} at {when}");
@ -122,6 +151,7 @@ pub async fn get_signup_success(
Path(id): Path<String>, Path(id): Path<String>,
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
) -> Response { ) -> Response {
dbg!(&id);
let id = id.trim(); let id = id.trim();
let id = Julid::from_string(id).unwrap_or_default(); let id = Julid::from_string(id).unwrap_or_default();
let user: User = { let user: User = {
@ -153,6 +183,7 @@ pub(crate) async fn create_user(
email: &Option<String>, email: &Option<String>,
password: &[u8], password: &[u8],
pool: &SqlitePool, pool: &SqlitePool,
invitation: &str,
) -> Result<User, CreateUserError> { ) -> Result<User, CreateUserError> {
// Argon2 with default params (Argon2id v19) // Argon2 with default params (Argon2id v19)
let argon2 = Argon2::default(); let argon2 = Argon2::default();
@ -162,15 +193,25 @@ pub(crate) async fn create_user(
.unwrap() // safe to unwrap, we know the salt is valid .unwrap() // safe to unwrap, we know the salt is valid
.to_string(); .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(username)
.bind(displayname) .bind(displayname)
.bind(email) .bind(email)
.bind(&pwhash) .bind(&pwhash)
.bind(invited_by)
.fetch_one(pool) .fetch_one(pool)
.await; .await
.map_err(|e| {
Ok(res.map_err(|e| {
match e { match e {
sqlx::Error::Database(db) => { sqlx::Error::Database(db) => {
let exit = db.code().unwrap_or_default().parse().unwrap_or(0); let exit = db.code().unwrap_or_default().parse().unwrap_or(0);
@ -183,7 +224,52 @@ pub(crate) async fn create_user(
} }
_ => 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<Julid, CreateUserErrorKind> {
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)] #[cfg(test)]
mod test { mod test {
use axum::http::StatusCode; use axum::http::StatusCode;
use julid::Julid;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use crate::{ use crate::{
db::get_db_pool, db::get_db_pool,
templates::{SignupPage, SignupSuccessPage}, signup::templates::{SignupPage, SignupSuccessPage},
test_utils::{massage, server_with_pool, FORM_CONTENT_TYPE}, test_utils::{massage, server_with_pool, FORM_CONTENT_TYPE, INVITE_ID_INT},
User, User,
}; };
const GOOD_FORM: &str = "username=good_user&displayname=Good+User&password=aaaa&pw_verify=aaaa";
#[test] #[test]
fn post_create_user() { fn post_create_user() {
let pool = get_db_pool(); let pool = get_db_pool();
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
let server = server_with_pool(&pool).await; 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 let resp = server
.post("/signup") .post("/signup")
@ -219,11 +306,14 @@ mod test {
.content_type(FORM_CONTENT_TYPE) .content_type(FORM_CONTENT_TYPE)
.await; .await;
dbg!(&resp.text());
assert_eq!(StatusCode::SEE_OTHER, resp.status_code()); assert_eq!(StatusCode::SEE_OTHER, resp.status_code());
// get the new user from the db // get the new user from the db
let user = User::try_get("good_user", &pool).await; let user = User::try_get("good_user", &pool).await;
assert!(user.is_ok()); assert!(user.is_ok());
assert!(user.unwrap().is_some());
}); });
} }
@ -233,8 +323,9 @@ mod test {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let invitation: Julid = INVITE_ID_INT.into();
let resp = server.get("/signup").await; let path = format!("/signup/{invitation}");
let resp = server.get(&path).await;
let body = std::str::from_utf8(resp.as_bytes()).unwrap(); let body = std::str::from_utf8(resp.as_bytes()).unwrap();
let expected = SignupPage::default().to_string(); let expected = SignupPage::default().to_string();
assert_eq!(&expected, body); assert_eq!(&expected, body);
@ -243,11 +334,15 @@ mod test {
#[test] #[test]
fn handle_signup_success() { fn handle_signup_success() {
dbg!("getting the pool");
let pool = get_db_pool(); let pool = get_db_pool();
dbg!("got the pool");
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
let server = server_with_pool(&pool).await; 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 let resp = server
.post("/signup") .post("/signup")
@ -259,9 +354,12 @@ mod test {
assert_eq!(StatusCode::SEE_OTHER, resp.status_code()); assert_eq!(StatusCode::SEE_OTHER, resp.status_code());
// get the new user from the db // 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 id = user.id;
let path = format!("/signup_success/{id}"); let path = format!("/signup_success/{id}");
let resp = server.get(&path).expect_success().await; let resp = server.get(&path).expect_success().await;
@ -276,19 +374,19 @@ mod test {
//-************************************************************************ //-************************************************************************
mod failure { mod failure {
use super::*; use super::*;
use crate::signup::{CreateUserError, CreateUserErrorKind}; use crate::signup::handlers::{CreateUserError, CreateUserErrorKind};
// various ways to fuck up signup // various ways to fuck up signup
const PASSWORD_MISMATCH_FORM: &str = 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 = const PASSWORD_SHORT_FORM: &str =
"username=bad_user&displayname=Bad+User&password=a&pw_verify=a"; "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"; 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 = 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 = const USERNAME_LONG_FORM: &str =
"username=bad_user12345678901234567890&displayname=Bad+User&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"; 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] #[test]
fn password_mismatch() { fn password_mismatch() {
@ -301,7 +399,7 @@ mod test {
let resp = server let resp = server
.post("/signup") .post("/signup")
// failure to sign up is not failure to submit the request // failure to sign up is not failure to submit the request
.expect_success() //.expect_success()
.bytes(body) .bytes(body)
.content_type(FORM_CONTENT_TYPE) .content_type(FORM_CONTENT_TYPE)
.await; .await;
@ -381,7 +479,7 @@ mod test {
rt.block_on(async { rt.block_on(async {
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let form = 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 body = massage(&form);
let resp = server let resp = server
@ -460,7 +558,10 @@ mod test {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
let server = server_with_pool(&pool).await; 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 let _resp = server
.post("/signup") .post("/signup")

72
src/signup/mod.rs Normal file
View File

@ -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<i64>,
remaining: i16,
}
impl Invitation {
pub async fn commit(&self, db: &SqlitePool) -> Result<Julid, CreateInviteError> {
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,
}
}
}

28
src/signup/templates.rs Normal file
View File

@ -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<String>,
pub email: Option<String>,
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);

View File

@ -3,22 +3,6 @@ use serde::{Deserialize, Serialize};
use crate::{OptionalOptionalUser, User}; 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<String>,
pub email: Option<String>,
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)] #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)]
#[template(path = "login_page.html")] #[template(path = "login_page.html")]
pub struct LoginPage { pub struct LoginPage {

View File

@ -7,10 +7,16 @@ use crate::User;
pub const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded"; pub const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded";
pub async fn server_with_pool(pool: &SqlitePool) -> TestServer { pub const INVITE_ID_INT: u128 = 42;
let user = get_test_user();
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") let r: i32 = sqlx::query_scalar("select count(*) from users")
.fetch_one(pool) .fetch_one(pool)
.await .await
@ -33,19 +39,17 @@ fn get_test_user() -> User {
pwhash: "$argon2id$v=19$m=19456,t=2,p=1$GWsCH1w5RYaP9WWmq+xw0g$hmOEqC+MU+vnEk3bOdkoE+z01mOmmOeX08XyPyjqua8".to_string(), pwhash: "$argon2id$v=19$m=19456,t=2,p=1$GWsCH1w5RYaP9WWmq+xw0g$hmOEqC+MU+vnEk3bOdkoE+z01mOmmOeX08XyPyjqua8".to_string(),
id: Julid::omega(), id: Julid::omega(),
displayname: Some("Test User".to_string()), displayname: Some("Test User".to_string()),
invited_by: Julid::omega(),
..Default::default() ..Default::default()
} }
} }
async fn insert_user_with_id(user: &User, pool: &SqlitePool) { async fn add_test_invite(pool: &SqlitePool) {
sqlx::query( let id: Julid = INVITE_ID_INT.into();
"insert into users (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)", sqlx::query("insert into invites (id, owner, remaining) values (?, ?, ?)")
) .bind(id)
.bind(user.id) .bind(Julid::omega())
.bind(&user.username) .bind(2)
.bind(&user.displayname)
.bind(&user.email)
.bind(&user.pwhash)
.execute(pool) .execute(pool)
.await .await
.unwrap(); .unwrap();

View File

@ -13,6 +13,8 @@ use crate::AuthSession;
const USERNAME_QUERY: &str = "select * from users where username = $1"; 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 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)] #[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct User { pub struct User {
@ -22,6 +24,7 @@ pub struct User {
pub email: Option<String>, pub email: Option<String>,
pub last_seen: Option<i64>, pub last_seen: Option<i64>,
pub pwhash: String, pub pwhash: String,
pub invited_by: Julid,
pub digest: String, pub digest: String,
} }
@ -35,6 +38,7 @@ impl sqlx::FromRow<'_, SqliteRow> for User {
displayname: row.try_get("displayname")?, displayname: row.try_get("displayname")?,
email: row.try_get("email")?, email: row.try_get("email")?,
last_seen: row.try_get("last_seen")?, last_seen: row.try_get("last_seen")?,
invited_by: row.try_get("invited_by")?,
pwhash, pwhash,
digest, digest,
}) })
@ -50,6 +54,7 @@ impl Debug for User {
.field("email", &self.email) .field("email", &self.email)
.field("last_seen", &self.last_seen) .field("last_seen", &self.last_seen)
.field("digest", &self.digest) .field("digest", &self.digest)
.field("invited_by", &self.invited_by.as_string())
.finish() .finish()
} }
} }
@ -75,6 +80,19 @@ impl User {
.await .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) { pub async fn update_last_seen(&self, pool: &SqlitePool) {
match sqlx::query(LAST_SEEN_QUERY) match sqlx::query(LAST_SEEN_QUERY)
.bind(self.id) .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()
}
}
} }
//-************************************************************************ //-************************************************************************

View File

@ -249,8 +249,7 @@ pub async fn get_search_watch(
let query = if search_query == SearchQuery::default() { let query = if search_query == SearchQuery::default() {
query_as(DEFAULT_WATCHES_QUERY) query_as(DEFAULT_WATCHES_QUERY)
} else { } else if let Some(title) = search_query.title {
if let Some(title) = search_query.title {
let q = format!("%{title}%"); let q = format!("%{title}%");
query_as("select * from watches where title like ?").bind(q) query_as("select * from watches where title like ?").bind(q)
} else if let Some(search) = search_query.search { } else if let Some(search) = search_query.search {
@ -258,7 +257,6 @@ pub async fn get_search_watch(
query_as("select * from watches where title like ?").bind(q) query_as("select * from watches where title like ?").bind(q)
} else { } else {
query_as(DEFAULT_WATCHES_QUERY) query_as(DEFAULT_WATCHES_QUERY)
}
}; };
// until tantivy search // until tantivy search

View File

@ -28,7 +28,7 @@
<div class="watchlist"> <div class="watchlist">
<ul> <ul>
{% for watch in watches %} {% for watch in watches %}
<li><span class="watchtitle">{{watch.title}}</span> -- {% call m::get_or_default(watch.year(), "when??") %}: <li><span class="watchtitle">{{watch.title}}</span> -- {% call m::get_or_default(watch.year(), "when??") %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Dang, Bish{% endblock %}
{% block content %}
{% block header %}{% endblock %}
<h1>Oh dang!</h1>
<div id="signup_success">
<p>
Sorry, something went wrong: {{self.0}}
</p>
</div>
{% endblock %}