Work
This commit is contained in:
parent
f08b3e0bcb
commit
6a1cb04428
13 changed files with 282 additions and 208 deletions
163
Cargo.lock
generated
163
Cargo.lock
generated
|
@ -25,7 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9dddf0af1514897dc80a11a084aa032ec7c1c042af454f502aa81c49af0a25fb"
|
checksum = "9dddf0af1514897dc80a11a084aa032ec7c1c042af454f502aa81c49af0a25fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"base64 0.13.0",
|
"base64",
|
||||||
"cookie",
|
"cookie",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -66,7 +66,7 @@ dependencies = [
|
||||||
"actix-tls",
|
"actix-tls",
|
||||||
"actix-utils",
|
"actix-utils",
|
||||||
"ahash",
|
"ahash",
|
||||||
"base64 0.13.0",
|
"base64",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"brotli2",
|
"brotli2",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -326,7 +326,7 @@ checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"version_check 0.9.3",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -359,18 +359,23 @@ version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ascii_utils"
|
|
||||||
version = "0.9.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_escape"
|
name = "askama_escape"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb"
|
checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.51"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atoi"
|
name = "atoi"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -392,15 +397,6 @@ version = "0.2.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
|
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "base64"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
|
@ -475,12 +471,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bufstream"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
|
@ -569,7 +559,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d"
|
checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"base64 0.13.0",
|
"base64",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"hmac",
|
"hmac",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
@ -577,7 +567,7 @@ dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"time",
|
"time",
|
||||||
"version_check 0.9.3",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -761,12 +751,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
|
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fast_chemail"
|
name = "fastrand"
|
||||||
version = "0.9.6"
|
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 = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4"
|
checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ascii_utils",
|
"instant",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -939,7 +929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check 0.9.3",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1061,12 +1051,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hostname"
|
name = "hostname"
|
||||||
version = "0.1.5"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e"
|
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"winutil",
|
"match_cfg",
|
||||||
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1092,6 +1083,12 @@ version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
|
checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
@ -1160,20 +1157,31 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lettre"
|
name = "lettre"
|
||||||
version = "0.9.6"
|
version = "0.10.0-rc.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86ed8677138975b573ab4949c35613931a4addeadd0a8a6aa0327e2a979660de"
|
checksum = "d8697ded52353bdd6fec234b3135972433397e86d0493d9fc38fbf407b7c106a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.10.1",
|
"async-trait",
|
||||||
"bufstream",
|
"base64",
|
||||||
"fast_chemail",
|
"fastrand",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
"hostname",
|
"hostname",
|
||||||
"log",
|
"httpdate",
|
||||||
|
"idna",
|
||||||
|
"mime",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"nom 4.2.3",
|
"nom",
|
||||||
|
"once_cell",
|
||||||
|
"quoted_printable",
|
||||||
|
"r2d2",
|
||||||
|
"regex",
|
||||||
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"tokio",
|
||||||
"serde_json",
|
"tokio-rustls",
|
||||||
|
"webpki",
|
||||||
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1260,6 +1268,12 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "match_cfg"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
@ -1347,16 +1361,6 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "4.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"version_check 0.1.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "6.1.2"
|
version = "6.1.2"
|
||||||
|
@ -1367,7 +1371,7 @@ dependencies = [
|
||||||
"funty",
|
"funty",
|
||||||
"lexical-core",
|
"lexical-core",
|
||||||
"memchr",
|
"memchr",
|
||||||
"version_check 0.9.3",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1630,6 +1634,23 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quoted_printable"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1238256b09923649ec89b08104c4dfe9f6cb2fea734a5db5384e44916d59e9c5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r2d2"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"parking_lot",
|
||||||
|
"scheduled-thread-pool",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "radium"
|
name = "radium"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
@ -1759,7 +1780,7 @@ version = "0.19.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
|
checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64",
|
||||||
"log",
|
"log",
|
||||||
"ring",
|
"ring",
|
||||||
"sct",
|
"sct",
|
||||||
|
@ -1791,6 +1812,15 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scheduled-thread-pool"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7"
|
||||||
|
dependencies = [
|
||||||
|
"parking_lot",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -2023,7 +2053,7 @@ checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"maplit",
|
"maplit",
|
||||||
"nom 6.1.2",
|
"nom",
|
||||||
"regex",
|
"regex",
|
||||||
"unicode_categories",
|
"unicode_categories",
|
||||||
]
|
]
|
||||||
|
@ -2124,7 +2154,7 @@ version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff"
|
checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"version_check 0.9.3",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2269,7 +2299,7 @@ dependencies = [
|
||||||
"standback",
|
"standback",
|
||||||
"stdweb",
|
"stdweb",
|
||||||
"time-macros",
|
"time-macros",
|
||||||
"version_check 0.9.3",
|
"version_check",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2480,7 +2510,7 @@ version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
|
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"version_check 0.9.3",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2550,12 +2580,6 @@ version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
|
@ -2703,15 +2727,6 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winutil"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wyz"
|
name = "wyz"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
|
@ -13,7 +13,7 @@ actix-session = "0.5.0-beta.2"
|
||||||
actix-web = { version = "4.0.0-beta.8", features = [ "rustls" ] }
|
actix-web = { version = "4.0.0-beta.8", features = [ "rustls" ] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
handlebars = { version = "4", features = [ "dir_source" ] }
|
handlebars = { version = "4", features = [ "dir_source" ] }
|
||||||
lettre = { version = "0.9", features = [ "serde-impls" ] }
|
lettre = { version = "0.10.0-rc.3", features = [ "serde", "tokio1-rustls-tls" ] }
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
rand = { version = "0.8", features = [ "small_rng" ] }
|
rand = { version = "0.8", features = [ "small_rng" ] }
|
||||||
serde = "1"
|
serde = "1"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
CREATE TABLE IF NOT EXISTS users(
|
CREATE TABLE IF NOT EXISTS users(
|
||||||
email TEXT PRIMARY KEY,
|
email TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
password BLOB NOT NULL
|
password BLOB NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
{
|
{
|
||||||
"db": "SQLite",
|
"db": "SQLite",
|
||||||
|
"15fac42882fc4be06e0f99d3be97fcbf1570c6bd14fcd13cd96ee78892668489": {
|
||||||
|
"query": "CREATE TABLE IF NOT EXISTS users(\n email TEXT NOT NULL UNIQUE PRIMARY KEY,\n password BLOB NOT NULL\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS ix_email ON users(email);",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
}
|
||||||
|
},
|
||||||
"57f6668b1fb93316e1beff8c2189a59da3a34995afc962a4704bc7d196a159f3": {
|
"57f6668b1fb93316e1beff8c2189a59da3a34995afc962a4704bc7d196a159f3": {
|
||||||
"query": "INSERT INTO users (email, password) VALUES (?, ?)",
|
"query": "INSERT INTO users (email, password) VALUES (?, ?)",
|
||||||
"describe": {
|
"describe": {
|
||||||
|
@ -27,15 +37,5 @@
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"dd98414343cc029a0cc106382532de954050ded749f3a3f7d9dc077ed7515572": {
|
|
||||||
"query": "CREATE TABLE IF NOT EXISTS users(\n email TEXT PRIMARY KEY,\n password BLOB NOT NULL\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS ix_email ON users(email);",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 0
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
20
src/email.rs
Normal file
20
src/email.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use lettre::error::Error as EmailError;
|
||||||
|
use lettre::message::Mailbox;
|
||||||
|
use lettre::transport::stub::StubTransport;
|
||||||
|
use lettre::{Address, AsyncTransport, Message};
|
||||||
|
|
||||||
|
pub async fn send_registration_email(address: Address) -> Result<(), EmailError> {
|
||||||
|
let message = Message::builder()
|
||||||
|
.from(Mailbox::new(
|
||||||
|
Some("Some username".to_string()),
|
||||||
|
"foo@example.com".parse().unwrap(),
|
||||||
|
))
|
||||||
|
.to(Mailbox::new(None, address))
|
||||||
|
.subject("Registration for this website")
|
||||||
|
.body("hell world".to_string())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
StubTransport::new_ok().send(message).await.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
166
src/main.rs
166
src/main.rs
|
@ -1,8 +1,12 @@
|
||||||
|
mod email;
|
||||||
|
mod password;
|
||||||
mod session;
|
mod session;
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use session::Session;
|
use crate::email::send_registration_email;
|
||||||
|
use crate::password::Password;
|
||||||
|
use crate::session::Session;
|
||||||
|
|
||||||
use actix_csrf::extractor::{CsrfCookie, CsrfToken};
|
use actix_csrf::extractor::{CsrfCookie, CsrfToken};
|
||||||
use actix_csrf::Csrf;
|
use actix_csrf::Csrf;
|
||||||
|
@ -10,15 +14,14 @@ use actix_session::CookieSession;
|
||||||
use actix_web::cookie::SameSite;
|
use actix_web::cookie::SameSite;
|
||||||
use actix_web::error::InternalError;
|
use actix_web::error::InternalError;
|
||||||
use actix_web::http::header::LOCATION;
|
use actix_web::http::header::LOCATION;
|
||||||
use actix_web::http::{Method, StatusCode};
|
use actix_web::http::{Method, StatusCode, Uri};
|
||||||
use actix_web::middleware::Logger;
|
use actix_web::middleware::Logger;
|
||||||
use actix_web::web::{Form, Query};
|
use actix_web::web::{Form, Query};
|
||||||
use actix_web::{get, post, App, HttpRequest, HttpResponse, HttpServer, Responder};
|
use actix_web::{get, post, App, HttpRequest, HttpResponse, HttpServer, Responder};
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
use lettre::EmailAddress;
|
use lettre::Address as EmailAddress;
|
||||||
use once_cell::sync::{Lazy, OnceCell};
|
use once_cell::sync::{Lazy, OnceCell};
|
||||||
use rand::prelude::StdRng;
|
use rand::prelude::StdRng;
|
||||||
use serde::de::Visitor;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sodiumoxide::crypto::pwhash::argon2id13::{self, HashedPassword, HASHEDPASSWORDBYTES};
|
use sodiumoxide::crypto::pwhash::argon2id13::{self, HashedPassword, HASHEDPASSWORDBYTES};
|
||||||
use sqlx::sqlite::SqliteConnectOptions;
|
use sqlx::sqlite::SqliteConnectOptions;
|
||||||
|
@ -80,6 +83,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
.service(login)
|
.service(login)
|
||||||
.service(register_ui)
|
.service(register_ui)
|
||||||
.service(register)
|
.service(register)
|
||||||
|
.service(registration_confirmation)
|
||||||
.service(account_ui)
|
.service(account_ui)
|
||||||
.service(logout)
|
.service(logout)
|
||||||
.service(actix_files::Files::new("/static", "src/static"))
|
.service(actix_files::Files::new("/static", "src/static"))
|
||||||
|
@ -89,20 +93,25 @@ async fn main() -> std::io::Result<()> {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
/// Macro meme to render a template without any context or with a provided one.
|
||||||
enum SessionState {
|
macro_rules! render {
|
||||||
Anonymous,
|
($template_name:literal) => {
|
||||||
}
|
render!($template_name, &())
|
||||||
|
};
|
||||||
#[get("/")]
|
($template_name:literal, $template_args:expr $(,)?) => {
|
||||||
async fn index() -> impl Responder {
|
match TEMPLATE_ENGINE.render($template_name, $template_args) {
|
||||||
match TEMPLATE_ENGINE.render("index", &()) {
|
|
||||||
Ok(resp) => Ok(HttpResponse::Ok().body(resp)),
|
Ok(resp) => Ok(HttpResponse::Ok().body(resp)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
Err(InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))
|
Err(InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index() -> impl Responder {
|
||||||
|
render!("index")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -118,19 +127,13 @@ async fn login_ui(csrf: CsrfToken, mut query: Query<LoginQuery>) -> impl Respond
|
||||||
csrf: CsrfToken,
|
csrf: CsrfToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
match TEMPLATE_ENGINE.render(
|
render!(
|
||||||
"login",
|
"login",
|
||||||
&TemplateArgs {
|
&TemplateArgs {
|
||||||
error: query.error.take(),
|
error: query.error.take(),
|
||||||
csrf,
|
csrf,
|
||||||
},
|
},
|
||||||
) {
|
)
|
||||||
Ok(resp) => Ok(HttpResponse::Ok().body(resp)),
|
|
||||||
Err(e) => {
|
|
||||||
error!("{}", e);
|
|
||||||
Err(InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -140,42 +143,6 @@ struct Login {
|
||||||
password: Password,
|
password: Password,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
struct Password(String);
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Password {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
use serde::de::{Error, Unexpected};
|
|
||||||
|
|
||||||
struct SecretDeserializer;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for SecretDeserializer {
|
|
||||||
type Value = Password;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("a password between 8 and 64 bytes")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
|
|
||||||
if v.len() < 8 || v.len() > 64 {
|
|
||||||
println!("password failed");
|
|
||||||
return Err(Error::invalid_value(
|
|
||||||
Unexpected::Str("password with invalid size"),
|
|
||||||
&"a password between 8 and 64 bytes",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Password(v.to_owned()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_string(SecretDeserializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/login")]
|
#[post("/login")]
|
||||||
async fn login(csrf_cookie: CsrfCookie, form: Form<Login>, session: Session) -> impl Responder {
|
async fn login(csrf_cookie: CsrfCookie, form: Form<Login>, session: Session) -> impl Responder {
|
||||||
if !csrf_cookie.validate(form.csrf.as_ref()) {
|
if !csrf_cookie.validate(form.csrf.as_ref()) {
|
||||||
|
@ -188,23 +155,18 @@ async fn login(csrf_cookie: CsrfCookie, form: Form<Login>, session: Session) ->
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(record) = verified {
|
if let Ok(record) = verified {
|
||||||
let verified = argon2id13::pwhash_verify(
|
if form
|
||||||
&HashedPassword::from_slice(&record.password).unwrap(),
|
.password
|
||||||
form.password.0.as_bytes(),
|
.verify(&HashedPassword::from_slice(&record.password).unwrap())
|
||||||
);
|
{
|
||||||
if verified {
|
|
||||||
let redirect_to = session.get_redirect_url();
|
let redirect_to = session.get_redirect_url();
|
||||||
session.init(&form.email);
|
session.init(&form.email);
|
||||||
|
return redirect(
|
||||||
let mut resp = HttpResponse::SeeOther();
|
&redirect_to
|
||||||
|
.as_ref()
|
||||||
if let Some(path) = redirect_to {
|
.map(Uri::to_string)
|
||||||
resp.insert_header((LOCATION, path.to_string()));
|
.unwrap_or("/account".into()),
|
||||||
} else {
|
);
|
||||||
resp.insert_header((LOCATION, "/account"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.finish();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// To guard against timing attacks, we'll construct a fake password to
|
// To guard against timing attacks, we'll construct a fake password to
|
||||||
|
@ -220,15 +182,11 @@ async fn login(csrf_cookie: CsrfCookie, form: Form<Login>, session: Session) ->
|
||||||
|
|
||||||
// Rust shouldn't optimize this out since it ultimately calls out to
|
// Rust shouldn't optimize this out since it ultimately calls out to
|
||||||
// a C function, so it shouldn't find out that the function is pure.
|
// a C function, so it shouldn't find out that the function is pure.
|
||||||
argon2id13::pwhash_verify(
|
form.password
|
||||||
&HashedPassword::from_slice(&data).unwrap(),
|
.verify(&HashedPassword::from_slice(&data).expect("Should be valid password data"));
|
||||||
form.password.0.as_bytes(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponse::SeeOther()
|
redirect("/login?error=true")
|
||||||
.insert_header((LOCATION, "/login?error=true"))
|
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/register")]
|
#[get("/register")]
|
||||||
|
@ -238,13 +196,7 @@ async fn register_ui(csrf: CsrfToken) -> impl Responder {
|
||||||
csrf: CsrfToken,
|
csrf: CsrfToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
match TEMPLATE_ENGINE.render("register", &TemplateArgs { csrf }) {
|
render!("register", &TemplateArgs { csrf })
|
||||||
Ok(resp) => Ok(HttpResponse::Ok().body(resp)),
|
|
||||||
Err(e) => {
|
|
||||||
error!("{}", e);
|
|
||||||
Err(InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -255,26 +207,15 @@ struct RegistrationInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/register")]
|
#[post("/register")]
|
||||||
async fn register(
|
async fn register(csrf_cookie: CsrfCookie, form: Form<RegistrationInfo>) -> impl Responder {
|
||||||
csrf_cookie: CsrfCookie,
|
|
||||||
form: Form<RegistrationInfo>,
|
|
||||||
session: Session,
|
|
||||||
) -> impl Responder {
|
|
||||||
if !csrf_cookie.validate(form.csrf.as_ref()) {
|
if !csrf_cookie.validate(form.csrf.as_ref()) {
|
||||||
return HttpResponse::BadRequest().finish();
|
return HttpResponse::BadRequest().finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
let hashed = {
|
let hashed = if let Ok(res) = form.password.hashed() {
|
||||||
let res = argon2id13::pwhash(
|
|
||||||
form.password.0.as_bytes(),
|
|
||||||
argon2id13::OPSLIMIT_INTERACTIVE,
|
|
||||||
argon2id13::MEMLIMIT_INTERACTIVE,
|
|
||||||
);
|
|
||||||
if let Ok(res) = res {
|
|
||||||
res
|
res
|
||||||
} else {
|
} else {
|
||||||
return HttpResponse::InternalServerError().finish();
|
return HttpResponse::InternalServerError().finish();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let hashed = hashed.as_ref();
|
let hashed = hashed.as_ref();
|
||||||
|
|
||||||
|
@ -288,31 +229,36 @@ async fn register(
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if insert_res.is_ok() {
|
if insert_res.is_ok() {
|
||||||
session.init(&form.email);
|
send_registration_email(form.email.clone()).await.unwrap();
|
||||||
|
|
||||||
HttpResponse::SeeOther()
|
|
||||||
.insert_header((LOCATION, "/account"))
|
|
||||||
.finish()
|
|
||||||
} else {
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redirect("/registration_confirmation")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/registration_confirmation")]
|
||||||
|
async fn registration_confirmation() -> impl Responder {
|
||||||
|
render!("registration_confirmation")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/account")]
|
#[get("/account")]
|
||||||
async fn account_ui(req: HttpRequest, session: Session) -> impl Responder {
|
async fn account_ui(req: HttpRequest, session: Session) -> impl Responder {
|
||||||
if let Err(error) = session.validate_or_redirect(req.uri()) {
|
if let Err(error) = session.validate_or_redirect(req.uri()) {
|
||||||
return error;
|
return Ok(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponse::Ok().body(format!("{:?}", session.email()))
|
render!("account")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/logout")]
|
#[get("/logout")]
|
||||||
async fn logout(session: Session) -> impl Responder {
|
async fn logout(session: Session) -> impl Responder {
|
||||||
// It should be ok to logout without a CSRF token; the worst case is that
|
// While this is a state-mutating endpoint, it is fine to not have a CSRF
|
||||||
// the user is logged out, which is fail-safe.
|
// token; the worst case is that the user is logged out, which is fail-safe.
|
||||||
session.purge();
|
session.purge();
|
||||||
|
redirect("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redirect(location: &str) -> HttpResponse {
|
||||||
HttpResponse::SeeOther()
|
HttpResponse::SeeOther()
|
||||||
.append_header((LOCATION, "/"))
|
.append_header((LOCATION, location))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
63
src/password.rs
Normal file
63
src/password.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use serde::de::Visitor;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sodiumoxide::crypto::pwhash::argon2id13::{self, HashedPassword};
|
||||||
|
use sodiumoxide::utils::memzero;
|
||||||
|
|
||||||
|
#[derive(Serialize, PartialEq, Eq)]
|
||||||
|
pub struct Password(String);
|
||||||
|
|
||||||
|
impl Password {
|
||||||
|
pub fn hashed(&self) -> Result<HashedPassword, ()> {
|
||||||
|
argon2id13::pwhash(
|
||||||
|
self.0.as_bytes(),
|
||||||
|
argon2id13::OPSLIMIT_INTERACTIVE,
|
||||||
|
argon2id13::MEMLIMIT_INTERACTIVE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self, password: &HashedPassword) -> bool {
|
||||||
|
argon2id13::pwhash_verify(password, self.0.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A custom drop implementation is necessary for this type as we need to ensure
|
||||||
|
/// that the password is not stored in memory for an extended period of time.
|
||||||
|
impl Drop for Password {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: self.0 is never accessed after calling drop.
|
||||||
|
memzero(unsafe { self.0.as_bytes_mut() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Password {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::de::{Error, Unexpected};
|
||||||
|
|
||||||
|
struct SecretDeserializer;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for SecretDeserializer {
|
||||||
|
type Value = Password;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a password between 8 and 64 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||||
|
if v.len() < 8 || v.len() > 64 {
|
||||||
|
println!("password failed");
|
||||||
|
return Err(Error::invalid_value(
|
||||||
|
Unexpected::Str("password with invalid size"),
|
||||||
|
&"a password between 8 and 64 bytes",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Password(v.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_string(SecretDeserializer)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ use actix_web::dev::Payload;
|
||||||
use actix_web::http::header::LOCATION;
|
use actix_web::http::header::LOCATION;
|
||||||
use actix_web::http::Uri;
|
use actix_web::http::Uri;
|
||||||
use actix_web::{FromRequest, HttpRequest, HttpResponse};
|
use actix_web::{FromRequest, HttpRequest, HttpResponse};
|
||||||
use lettre::EmailAddress;
|
use lettre::Address;
|
||||||
use rand::{thread_rng, Fill};
|
use rand::{thread_rng, Fill};
|
||||||
|
|
||||||
pub struct Session(actix_session::Session);
|
pub struct Session(actix_session::Session);
|
||||||
|
@ -26,7 +26,7 @@ impl FromRequest for Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn init(&self, email: &EmailAddress) {
|
pub fn init(&self, email: &Address) {
|
||||||
self.0.clear();
|
self.0.clear();
|
||||||
self.0
|
self.0
|
||||||
.insert("email", email)
|
.insert("email", email)
|
||||||
|
@ -77,7 +77,7 @@ impl Session {
|
||||||
.expect("setting a str to work");
|
.expect("setting a str to work");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn email(&self) -> Option<EmailAddress> {
|
pub fn email(&self) -> Option<Address> {
|
||||||
self.0.get("email").ok().flatten()
|
self.0.get("email").ok().flatten()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: 'M PLUS 1p', sans-serif;
|
font-family: 'M PLUS 1p', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
p, label, input {
|
p, label, input, a {
|
||||||
font-family: 'Noto Sans', sans-serif;
|
font-family: 'Noto Sans', sans-serif;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
10
src/templates/account.hbs
Normal file
10
src/templates/account.hbs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head }}
|
||||||
|
<body>
|
||||||
|
<h1>A TLD for everyone</h1>
|
||||||
|
|
||||||
|
<h2>Logged in!</h2>
|
||||||
|
<a href="/logout">Logout</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -5,5 +5,6 @@
|
||||||
<h1>A TLD for everyone</h1>
|
<h1>A TLD for everyone</h1>
|
||||||
|
|
||||||
<h2>About us</h2>
|
<h2>About us</h2>
|
||||||
|
<a href="/login">Login</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -6,9 +6,6 @@
|
||||||
<h1>A TLD for everyone</h1>
|
<h1>A TLD for everyone</h1>
|
||||||
|
|
||||||
<h2>Sign up</h2>
|
<h2>Sign up</h2>
|
||||||
{{#if error}}
|
|
||||||
<p>The email is already used. Please try again.</p>
|
|
||||||
{{/if}}
|
|
||||||
<form action="/register" method="post">
|
<form action="/register" method="post">
|
||||||
<input name="csrf" type="hidden" value="{{csrf}}">
|
<input name="csrf" type="hidden" value="{{csrf}}">
|
||||||
<label>Email
|
<label>Email
|
||||||
|
|
22
src/templates/registration_confirmation.hbs
Normal file
22
src/templates/registration_confirmation.hbs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head }}
|
||||||
|
<body>
|
||||||
|
<h1>A TLD for everyone</h1>
|
||||||
|
|
||||||
|
<h2>Registration Requested</h2>
|
||||||
|
<p>
|
||||||
|
If successful, a link to activate your account has been emailed to the address provided.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You must register your account before you can log in.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>I didn't receive an email!</h4>
|
||||||
|
<p>
|
||||||
|
An email will not have been sent if you already have an existing account
|
||||||
|
with us. If this is your first time registering, please also check your spam
|
||||||
|
folder.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue