🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-10-04 2:38:21.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-10-04 2:39:27.0 +01:00:00
commit
bcfeb8a72e0837fdf05db9ef05a76261c224583e [patch]
tree
5d498e494a5cc5577cc49777246b01786ae5cdcb
parent
5bd04a5fb46f7e2bcff4e0d4db4dd13cc405d5e5
download
bcfeb8a72e0837fdf05db9ef05a76261c224583e.tar.gz

OAuth support in the backend



Diff

 Cargo.lock                                               | 657 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 chartered-fs/Cargo.toml                                  |   2 ++
 chartered-web/Cargo.toml                                 |   5 +++++
 chartered-db/src/lib.rs                                  |   6 ++++--
 chartered-db/src/users.rs                                |  26 ++++++++++++++++++++++++++
 chartered-frontend/src/index.sass                        |  18 +++++++++++++++++-
 chartered-frontend/src/useAuth.tsx                       |   6 +++++-
 chartered-frontend/src/util.tsx                          |  25 +++++++++++++++++++++++++
 chartered-fs/src/lib.rs                                  |  52 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 chartered-web/src/config.rs                              |  88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 chartered-web/src/main.rs                                | 135 ++++++++++++++++++++++++++++++++++++++------------------------------------------
 book/src/getting-started/installation.md                 |   3 ++-
 chartered-frontend/src/pages/Login.tsx                   |  31 +++++++++++++++++++++----------
 chartered-web/src/endpoints/cargo_api/mod.rs             |  33 ++++++++++++++++++++++++++++-----
 chartered-web/src/endpoints/web_api/login.rs             |  75 ---------------------------------------------------------------------------
 chartered-web/src/endpoints/web_api/mod.rs               |  47 +++++++++++++++++++++++++++++++++++++++++------
 chartered-web/src/endpoints/web_api/auth/mod.rs          |  68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 chartered-web/src/endpoints/web_api/auth/openid.rs       | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 chartered-web/src/endpoints/web_api/auth/password.rs     |  45 +++++++++++++++++++++++++++++++++++++++++++++
 chartered-web/src/endpoints/web_api/crates/mod.rs        |  30 +++++++++++++++++++++++++-----
 chartered-web/src/endpoints/web_api/organisations/mod.rs |  30 +++++++++++++++++++++++++-----
 21 files changed, 1336 insertions(+), 214 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 59835fe..733132f 100644
--- a/Cargo.lock
+++ a/Cargo.lock
@@ -9,6 +9,16 @@
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"

[[package]]
name = "aead"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
dependencies = [
 "generic-array",
 "rand_core",
]

[[package]]
name = "aes"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -115,6 +125,21 @@
 "pbkdf2",
 "sha2",
 "zeroize",
]

[[package]]
name = "biscuit"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dee631cea28b00e115fd355a1adedc860b155096941dc01259969eabd434a37"
dependencies = [
 "chrono",
 "data-encoding",
 "num",
 "once_cell",
 "ring",
 "serde",
 "serde_json",
]

[[package]]
@@ -164,6 +189,12 @@
 "cipher",
 "opaque-debug",
]

[[package]]
name = "bumpalo"
version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538"

[[package]]
name = "byteorder"
@@ -188,6 +219,31 @@
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "chacha20"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01b72a433d0cf2aef113ba70f62634c56fddb0f244e6377185c56a7cadbd8f91"
dependencies = [
 "cfg-if",
 "cipher",
 "cpufeatures",
 "zeroize",
]

[[package]]
name = "chacha20poly1305"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b84ed6d1d5f7aa9bdde921a5090e0ca4d934d250ea3b402a5fab3a994e28a2a"
dependencies = [
 "aead",
 "chacha20",
 "cipher",
 "poly1305",
 "zeroize",
]

[[package]]
name = "chartered-db"
@@ -222,8 +278,10 @@
version = "0.1.0"
dependencies = [
 "async-trait",
 "itertools",
 "serde",
 "tokio",
 "url",
 "uuid",
]

@@ -269,7 +327,9 @@
version = "0.1.0"
dependencies = [
 "axum",
 "base64",
 "bytes",
 "chacha20poly1305",
 "chartered-db",
 "chartered-fs",
 "chartered-types",
@@ -281,7 +341,10 @@
 "log",
 "nom",
 "once_cell",
 "openid",
 "rand",
 "regex",
 "reqwest",
 "serde",
 "serde_json",
 "sha2",
@@ -319,6 +382,22 @@
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d"

[[package]]
name = "core-foundation"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
dependencies = [
 "core-foundation-sys",
 "libc",
]

[[package]]
name = "core-foundation-sys"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"

[[package]]
name = "cpufeatures"
@@ -487,6 +566,15 @@
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"

[[package]]
name = "encoding_rs"
version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
dependencies = [
 "cfg-if",
]

[[package]]
name = "env_logger"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -516,6 +604,21 @@
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"

[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
 "foreign-types-shared",
]

[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"

[[package]]
name = "form_urlencoded"
@@ -662,7 +765,32 @@
 "cfg-if",
 "libc",
 "wasi",
]

[[package]]
name = "h2"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c06815895acec637cd6ed6e9662c935b866d20a106f8361892893a7d9234964"
dependencies = [
 "bytes",
 "fnv",
 "futures-core",
 "futures-sink",
 "futures-util",
 "http",
 "indexmap",
 "slab",
 "tokio",
 "tokio-util",
 "tracing",
]

[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"

[[package]]
name = "headers"
@@ -773,6 +901,7 @@
 "futures-channel",
 "futures-core",
 "futures-util",
 "h2",
 "http",
 "http-body",
 "httparse",
@@ -784,6 +913,46 @@
 "tower-service",
 "tracing",
 "want",
]

[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
 "bytes",
 "hyper",
 "native-tls",
 "tokio",
 "tokio-native-tls",
]

[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
 "matches",
 "unicode-bidi",
 "unicode-normalization",
]

[[package]]
name = "if_chain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"

[[package]]
name = "indexmap"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
 "autocfg",
 "hashbrown",
]

[[package]]
@@ -803,6 +972,12 @@
dependencies = [
 "cfg-if",
]

[[package]]
name = "ipnet"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"

[[package]]
name = "itertools"
@@ -818,6 +993,15 @@
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"

[[package]]
name = "js-sys"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
dependencies = [
 "wasm-bindgen",
]

[[package]]
name = "lazy_static"
@@ -953,6 +1137,24 @@
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
 "winapi",
]

[[package]]
name = "native-tls"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
dependencies = [
 "lazy_static",
 "libc",
 "log",
 "openssl",
 "openssl-probe",
 "openssl-sys",
 "schannel",
 "security-framework",
 "security-framework-sys",
 "tempfile",
]

[[package]]
@@ -973,6 +1175,31 @@
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
 "winapi",
]

[[package]]
name = "num"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f"
dependencies = [
 "num-bigint 0.3.3",
 "num-complex",
 "num-integer",
 "num-iter",
 "num-rational",
 "num-traits",
]

[[package]]
name = "num-bigint"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3"
dependencies = [
 "autocfg",
 "num-integer",
 "num-traits",
]

[[package]]
@@ -983,6 +1210,15 @@
dependencies = [
 "autocfg",
 "num-integer",
 "num-traits",
]

[[package]]
name = "num-complex"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5"
dependencies = [
 "num-traits",
]

@@ -991,8 +1227,31 @@
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
 "autocfg",
 "num-traits",
]

[[package]]
name = "num-iter"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
dependencies = [
 "autocfg",
 "num-integer",
 "num-traits",
]

[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
 "autocfg",
 "num-bigint 0.3.3",
 "num-integer",
 "num-traits",
]

@@ -1026,6 +1285,57 @@
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"

[[package]]
name = "openid"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab30a9456b3484c408d9708b6f65b2bd834fdf22b73567775e1ca6de5524dd19"
dependencies = [
 "base64",
 "biscuit",
 "chrono",
 "lazy_static",
 "reqwest",
 "serde",
 "serde_json",
 "thiserror",
 "url",
 "validator",
]

[[package]]
name = "openssl"
version = "0.10.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a"
dependencies = [
 "bitflags",
 "cfg-if",
 "foreign-types",
 "libc",
 "once_cell",
 "openssl-sys",
]

[[package]]
name = "openssl-probe"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"

[[package]]
name = "openssl-sys"
version = "0.9.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058"
dependencies = [
 "autocfg",
 "cc",
 "libc",
 "pkg-config",
 "vcpkg",
]

[[package]]
name = "option_set"
@@ -1129,6 +1439,17 @@
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"

[[package]]
name = "poly1305"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede"
dependencies = [
 "cpufeatures",
 "opaque-debug",
 "universal-hash",
]

[[package]]
name = "ppv-lite86"
@@ -1143,6 +1464,30 @@
checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda"
dependencies = [
 "vcpkg",
]

[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
 "proc-macro-error-attr",
 "proc-macro2",
 "quote",
 "syn",
 "version_check",
]

[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
 "proc-macro2",
 "quote",
 "version_check",
]

[[package]]
@@ -1261,6 +1606,65 @@
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"

[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
 "winapi",
]

[[package]]
name = "reqwest"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22"
dependencies = [
 "base64",
 "bytes",
 "encoding_rs",
 "futures-core",
 "futures-util",
 "http",
 "http-body",
 "hyper",
 "hyper-tls",
 "ipnet",
 "js-sys",
 "lazy_static",
 "log",
 "mime",
 "native-tls",
 "percent-encoding",
 "pin-project-lite",
 "serde",
 "serde_json",
 "serde_urlencoded",
 "tokio",
 "tokio-native-tls",
 "url",
 "wasm-bindgen",
 "wasm-bindgen-futures",
 "web-sys",
 "winreg",
]

[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
 "cc",
 "libc",
 "once_cell",
 "spin",
 "untrusted",
 "web-sys",
 "winapi",
]

[[package]]
name = "ryu"
@@ -1275,6 +1679,16 @@
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
 "winapi-util",
]

[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
 "lazy_static",
 "winapi",
]

[[package]]
@@ -1291,6 +1705,29 @@
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"

[[package]]
name = "security-framework"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
dependencies = [
 "bitflags",
 "core-foundation",
 "core-foundation-sys",
 "libc",
 "security-framework-sys",
]

[[package]]
name = "security-framework-sys"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
dependencies = [
 "core-foundation-sys",
 "libc",
]

[[package]]
name = "serde"
@@ -1318,6 +1755,7 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [
 "indexmap",
 "itoa",
 "ryu",
 "serde",
@@ -1397,6 +1835,12 @@
 "libc",
 "winapi",
]

[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"

[[package]]
name = "subtle"
@@ -1422,6 +1866,20 @@
checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"

[[package]]
name = "tempfile"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
 "cfg-if",
 "libc",
 "rand",
 "redox_syscall",
 "remove_dir_all",
 "winapi",
]

[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1490,7 +1948,7 @@
 "hmac",
 "log",
 "md5",
 "num-bigint",
 "num-bigint 0.4.2",
 "num-integer",
 "pbkdf2",
 "rand",
@@ -1525,7 +1983,22 @@
dependencies = [
 "libc",
 "winapi",
]

[[package]]
name = "tinyvec"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
dependencies = [
 "tinyvec_macros",
]

[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"

[[package]]
name = "tokio"
@@ -1556,6 +2029,16 @@
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "tokio-native-tls"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
dependencies = [
 "native-tls",
 "tokio",
]

[[package]]
@@ -1689,6 +2172,21 @@
checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"

[[package]]
name = "unicode-bidi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"

[[package]]
name = "unicode-normalization"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [
 "tinyvec",
]

[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1705,6 +2203,35 @@
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"

[[package]]
name = "universal-hash"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
dependencies = [
 "generic-array",
 "subtle",
]

[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"

[[package]]
name = "url"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
 "form_urlencoded",
 "idna",
 "matches",
 "percent-encoding",
 "serde",
]

[[package]]
name = "uuid"
@@ -1713,8 +2240,47 @@
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
 "getrandom",
 "serde",
]

[[package]]
name = "validator"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d6937c33ec6039d8071bcf72933146b5bbe378d645d8fa59bdadabfc2a249"
dependencies = [
 "idna",
 "lazy_static",
 "regex",
 "serde",
 "serde_derive",
 "serde_json",
 "url",
 "validator_derive",
 "validator_types",
]

[[package]]
name = "validator_derive"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286b4497f270f59276a89ae0ad109d5f8f18c69b613e3fb22b61201aadb0c4d"
dependencies = [
 "if_chain",
 "lazy_static",
 "proc-macro-error",
 "proc-macro2",
 "quote",
 "regex",
 "syn",
 "validator_types",
]

[[package]]
name = "validator_types"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad9680608df133af2c1ddd5eaf1ddce91d60d61b6bc51494ef326458365a470a"

[[package]]
name = "vcpkg"
@@ -1756,6 +2322,84 @@
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"

[[package]]
name = "wasm-bindgen"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [
 "cfg-if",
 "serde",
 "serde_json",
 "wasm-bindgen-macro",
]

[[package]]
name = "wasm-bindgen-backend"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
dependencies = [
 "bumpalo",
 "lazy_static",
 "log",
 "proc-macro2",
 "quote",
 "syn",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-futures"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
dependencies = [
 "cfg-if",
 "js-sys",
 "wasm-bindgen",
 "web-sys",
]

[[package]]
name = "wasm-bindgen-macro"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
dependencies = [
 "quote",
 "wasm-bindgen-macro-support",
]

[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "wasm-bindgen-backend",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-shared"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"

[[package]]
name = "web-sys"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
dependencies = [
 "js-sys",
 "wasm-bindgen",
]

[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1785,6 +2429,15 @@
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

[[package]]
name = "winreg"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
dependencies = [
 "winapi",
]

[[package]]
name = "yasna"
@@ -1793,7 +2446,7 @@
checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75"
dependencies = [
 "bit-vec",
 "num-bigint",
 "num-bigint 0.4.2",
]

[[package]]
diff --git a/chartered-fs/Cargo.toml b/chartered-fs/Cargo.toml
index 5467201..3709d6e 100644
--- a/chartered-fs/Cargo.toml
+++ a/chartered-fs/Cargo.toml
@@ -7,8 +7,10 @@

[dependencies]
async-trait = "0.1"
itertools = "*"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["fs", "io-util"] }
url = "2"
uuid = { version = "0.8", features = ["v4", "serde"] }

[dev-dependencies]
diff --git a/chartered-web/Cargo.toml b/chartered-web/Cargo.toml
index 25a8848..7ef5394 100644
--- a/chartered-web/Cargo.toml
+++ a/chartered-web/Cargo.toml
@@ -11,7 +11,9 @@
chartered-types = { path = "../chartered-types" }

axum = { version = "0.2", features = ["headers"] }
base64 = "0.13"
bytes = "1"
chacha20poly1305 = { version = "0.9", features = ["std"] }
chrono = { version = "0.4", features = ["serde"] }
env_logger = "0.9"
futures = "0.3"
@@ -20,7 +22,10 @@
log = "0.4"
nom = "7"
once_cell = "1.8"
openid = "0.9"
rand = "*"
regex = "1.5"
reqwest = "0.11"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.9"
diff --git a/chartered-db/src/lib.rs b/chartered-db/src/lib.rs
index c35c0fc..22cf0a1 100644
--- a/chartered-db/src/lib.rs
+++ a/chartered-db/src/lib.rs
@@ -38,8 +38,10 @@
pub mod users;
pub mod uuid;

#[macro_use] extern crate diesel;
#[macro_use] extern crate diesel_migrations;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;

use diesel::{
    expression::{grouped::Grouped, AsExpression, Expression},
diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs
index 69e16d3..2470f2f 100644
--- a/chartered-db/src/users.rs
+++ a/chartered-db/src/users.rs
@@ -113,6 +113,32 @@
        .await?
    }

    pub async fn find_or_create(conn: ConnectionPool, given_username: String) -> Result<User> {
        use crate::schema::users::dsl::username;

        tokio::task::spawn_blocking(move || {
            let conn = conn.get()?;

            let user: Option<User> = crate::schema::users::table
                .filter(username.eq(&given_username))
                .get_result(&conn)
                .optional()?;

            if let Some(user) = user {
                return Ok(user);
            }

            diesel::insert_into(users::table)
                .values(username.eq(&given_username))
                .execute(&conn)?;

            Ok(crate::schema::users::table
                .filter(username.eq(given_username))
                .get_result(&conn)?)
        })
        .await?
    }

    /// Parses an ssh key from its `ssh-add -L` format (`ssh-ed25519 AAAAC3N...`) and

    /// inserts it to the database for the user.

    pub async fn insert_ssh_key(
diff --git a/chartered-frontend/src/index.sass b/chartered-frontend/src/index.sass
index c48704b..879e263 100644
--- a/chartered-frontend/src/index.sass
+++ a/chartered-frontend/src/index.sass
@@ -25,4 +25,20 @@
    white-space: nowrap

.text-decoration-underline-dotted
    text-decoration: underline dotted
    text-decoration: underline dotted

.side-lines
    display: grid
    grid-template-columns: minmax(20px, 1fr) auto minmax(20px, 1fr)
    align-items: center
    text-align: center
    grid-gap: 20px
    width: 100%

    &::before
        content: ""
        border-top: 1px solid

    &::after
        content: ""
        border-top: 1px solid
diff --git a/chartered-frontend/src/useAuth.tsx b/chartered-frontend/src/useAuth.tsx
index eec1f0b..acc0ccc 100644
--- a/chartered-frontend/src/useAuth.tsx
+++ a/chartered-frontend/src/useAuth.tsx
@@ -1,7 +1,11 @@
import React = require("react");
import { useState, useEffect, useContext, createContext } from "react";
import { unauthenticatedEndpoint } from "./util";

export interface OAuthProviders {
  providers: string[];
}

export interface AuthContext {
  login: (username: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
@@ -33,7 +37,7 @@
  }, [auth]);

  const login = async (username: string, password: string) => {
    let res = await fetch(unauthenticatedEndpoint("login"), {
    let res = await fetch(unauthenticatedEndpoint("login/password"), {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
diff --git a/chartered-frontend/src/util.tsx b/chartered-frontend/src/util.tsx
index b4c0e44..28e26a7 100644
--- a/chartered-frontend/src/util.tsx
+++ a/chartered-frontend/src/util.tsx
@@ -46,6 +46,31 @@
  return { response, error };
}

export function useUnauthenticatedRequest<S>(
  { endpoint }: { endpoint: string },
  reloadOn = []
): { response: S | null; error: string | null } {
  const [error, setError] = React.useState(null);
  const [response, setResponse] = React.useState(null);

  React.useEffect(async () => {
    try {
      let res = await fetch(unauthenticatedEndpoint(endpoint));
      let jsonRes = await res.json();

      if (jsonRes.error) {
        setError(jsonRes.error);
      } else {
        setResponse(jsonRes);
      }
    } catch (e) {
      setError(e.message);
    }
  }, reloadOn);

  return { response, error };
}

export function RoundedPicture({
  src,
  height,
diff --git a/chartered-fs/src/lib.rs b/chartered-fs/src/lib.rs
index a6e395d..309e238 100644
--- a/chartered-fs/src/lib.rs
+++ a/chartered-fs/src/lib.rs
@@ -1,22 +1,62 @@
#![deny(clippy::pedantic)]
#![deny(clippy::pedantic)]

use std::path::PathBuf;

use async_trait::async_trait;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use tokio::{
    fs::File,
    io::{AsyncReadExt, AsyncWriteExt},
};

#[derive(Debug)]
pub enum FS {
    S3 {
        host: String,
        bucket: String,
        path: String,
    },
    Local {
        path: PathBuf,
    },
}

impl std::str::FromStr for FS {
    type Err = url::ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let uri = url::Url::parse(s)?;

        Ok(match uri.scheme() {
            "s3" => {
                let mut path = uri.path_segments().unwrap();

                Self::S3 {
                    host: uri.host().unwrap().to_string(),
                    bucket: path.next().unwrap().to_string(),
                    path: path.intersperse("/").collect(),
                }
            }
            "file" => {
                panic!("{:#?}", uri);
            }
            _ => panic!("na"),
        })
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub enum FileSystemKind {
    Local,
    S3,
}

impl std::fmt::Display for FileSystemKind {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Self::Local => f.write_str("local"),
            Self::S3 => f.write_str("s3"),
        }
    }
}
@@ -105,7 +145,15 @@

#[cfg(test)]
mod tests {
    use super::FileSystem;
    use super::{FileSystem, FS};
    use std::str::FromStr;

    #[tokio::test]
    #[allow(clippy::pedantic)]
    async fn parse_filesystem() {
        // panic!("{:#?}", FS::from_str("s3://10.0.64.101:9000/my-bucket/my-location"));
        FS::from_str("file:///tmp/chartered");
    }

    #[tokio::test]
    #[allow(clippy::pedantic)]
diff --git a/chartered-web/src/config.rs b/chartered-web/src/config.rs
new file mode 100644
index 0000000..0a222c0 100644
--- /dev/null
+++ a/chartered-web/src/config.rs
@@ -1,0 +1,88 @@
use chacha20poly1305::Key as ChaCha20Poly1305Key;
use openid::DiscoveredClient;
use serde::{de::Error as SerdeDeError, Deserialize};
use std::collections::HashMap;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("Error discovering OpenID provider: {0}")]
    OpenId(#[from] openid::error::Error),
}

pub type OidcClients = HashMap<String, DiscoveredClient>;

#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct Config {
    pub auth: AuthConfig,
    #[serde(deserialize_with = "deserialize_encryption_key")]
    pub encryption_key: ChaCha20Poly1305Key,
}

impl Config {
    pub async fn create_oidc_clients(&self) -> Result<OidcClients, Error> {
        Ok(futures::future::try_join_all(
            self.auth
                .oauth
                .iter()
                .filter(|(_, config)| config.enabled)
                .map(|(name, config)| async move {
                    Ok::<_, Error>((
                        name.to_string(),
                        DiscoveredClient::discover(
                            config.client_id.to_string(),
                            config.client_secret.to_string(),
                            Some("http://127.0.0.1:1234/login/oauth".to_string()),
                            config.discovery_uri.clone(),
                        )
                        .await?,
                    ))
                }),
        )
        .await?
        .into_iter()
        .collect())
    }
}

#[derive(Deserialize, Default, Debug)]
pub struct AuthConfig {
    pub password: PasswordAuthConfig,
    #[serde(flatten)]
    pub oauth: HashMap<String, OAuthConfig>,
}

#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct PasswordAuthConfig {
    pub enabled: bool,
}

#[derive(Deserialize, Debug)]
pub struct OAuthConfig {
    pub enabled: bool,
    #[serde(deserialize_with = "deserialize_url")]
    pub discovery_uri: reqwest::Url,
    pub client_id: String,
    pub client_secret: String,
}

fn deserialize_encryption_key<'de, D: serde::Deserializer<'de>>(
    deserializer: D,
) -> Result<ChaCha20Poly1305Key, D::Error> {
    let key = String::deserialize(deserializer)?;

    if key.as_bytes().len() != 32 {
        return Err(D::Error::custom("encryption_key must be 32 bytes"));
    }

    Ok(ChaCha20Poly1305Key::clone_from_slice(key.as_bytes()))
}

fn deserialize_url<'de, D: serde::Deserializer<'de>>(
    deserializer: D,
) -> Result<reqwest::Url, D::Error> {
    let uri = String::deserialize(deserializer)?;
    reqwest::Url::parse(&uri).map_err(D::Error::custom)
}
diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs
index 57f478e..37fc215 100644
--- a/chartered-web/src/main.rs
+++ a/chartered-web/src/main.rs
@@ -1,14 +1,16 @@
#![deny(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]

mod config;
mod endpoints;
mod middleware;

use axum::{
    handler::{delete, get, patch, post, put},
    http::{Method, header},
    handler::get,
    http::{header, Method},
    AddExtensionLayer, Router,
};
use std::sync::Arc;
use tower::ServiceBuilder;
use tower_http::cors::{Any, CorsLayer};

@@ -22,111 +24,30 @@
// new route, the workaround is to box the router down to a
// dynamically dispatched version with every new route.
macro_rules! axum_box_after_every_route {
    (Router::new()$(.route($path:expr, $svc:expr$(,)?))*) => {
    (Router::new()
        $(.nest($nest_path:expr, $nest_svc:expr$(,)?))*
        $(.route($route_path:expr, $route_svc:expr$(,)?))*
    ) => {
        Router::new()
            $(
                .route($path, $svc)
                .nest($nest_path, $nest_svc)
                .boxed()
            )*
            $(
                .route($route_path, $route_svc)
                .boxed()
            )*
    };
}

pub(crate) use axum_box_after_every_route;

#[tokio::main]
#[allow(clippy::semicolon_if_nothing_returned)] // lint breaks with tokio::main
#[allow(clippy::too_many_lines)] // todo: refactor
async fn main() {
    env_logger::init();

    let pool = chartered_db::init().unwrap();

    let api_authenticated = axum_box_after_every_route!(Router::new()
        .route("/crates/new", put(endpoints::cargo_api::publish))
        .route("/crates/search", get(hello_world))
        .route(
            "/crates/:crate/owners",
            get(endpoints::cargo_api::get_owners)
        )
        .route("/crates/:crate/owners", put(hello_world))
        .route("/crates/:crate/owners", delete(hello_world))
        .route(
            "/crates/:crate/:version/yank",
            delete(endpoints::cargo_api::yank)
        )
        .route(
            "/crates/:crate/:version/unyank",
            put(endpoints::cargo_api::unyank)
        )
        .route(
            "/crates/:crate/:version/download",
            get(endpoints::cargo_api::download)
        ))
    .layer(
        ServiceBuilder::new()
            .layer_fn(middleware::auth::AuthMiddleware)
            .into_inner(),
    );

    let web_unauthenticated =
        axum_box_after_every_route!(Router::new().route("/login", post(endpoints::web_api::login)));

    let web_authenticated = axum_box_after_every_route!(Router::new()
        // organisations endpoints
        .route(
            "/organisations",
            get(endpoints::web_api::organisations::list)
        )
        .route(
            "/organisations",
            put(endpoints::web_api::organisations::create)
        )
        .route(
            "/organisations/:org",
            get(endpoints::web_api::organisations::info)
        )
        .route(
            "/organisations/:org/members",
            patch(endpoints::web_api::organisations::update_member)
        )
        .route(
            "/organisations/:org/members",
            put(endpoints::web_api::organisations::insert_member)
        )
        .route(
            "/organisations/:org/members",
            delete(endpoints::web_api::organisations::delete_member)
        )
        // crate endpoints
        .route("/crates/:org/:crate", get(endpoints::web_api::crates::info))
        .route(
            "/crates/:org/:crate/members",
            get(endpoints::web_api::crates::get_members)
        )
        .route(
            "/crates/:org/:crate/members",
            patch(endpoints::web_api::crates::update_member)
        )
        .route(
            "/crates/:org/:crate/members",
            put(endpoints::web_api::crates::insert_member)
        )
        .route(
            "/crates/:org/:crate/members",
            delete(endpoints::web_api::crates::delete_member)
        )
        .route(
            "/crates/recently-updated",
            get(endpoints::web_api::crates::list_recently_updated)
        )
        // users endpoints
        .route("/users/search", get(endpoints::web_api::search_users))
        .route("/ssh-key", get(endpoints::web_api::get_ssh_keys))
        .route("/ssh-key", put(endpoints::web_api::add_ssh_key))
        .route("/ssh-key/:id", delete(endpoints::web_api::delete_ssh_key)))
    .layer(
        ServiceBuilder::new()
            .layer_fn(middleware::auth::AuthMiddleware)
            .into_inner(),
    );

    let middleware_stack = ServiceBuilder::new()
        .layer_fn(middleware::logging::LoggingMiddleware)
@@ -134,9 +55,23 @@

    let app = Router::new()
        .route("/", get(hello_world))
        .nest("/a/:key/web/v1", web_authenticated)
        .nest("/a/-/web/v1", web_unauthenticated)
        .nest("/a/:key/o/:organisation/api/v1", api_authenticated)
        .nest(
            "/a/:key/web/v1",
            endpoints::web_api::authenticated_routes().layer(
                ServiceBuilder::new()
                    .layer_fn(crate::middleware::auth::AuthMiddleware)
                    .into_inner(),
            ),
        )
        .nest("/a/-/web/v1", endpoints::web_api::unauthenticated_routes())
        .nest(
            "/a/:key/o/:organisation/api/v1",
            endpoints::cargo_api::routes().layer(
                ServiceBuilder::new()
                    .layer_fn(crate::middleware::auth::AuthMiddleware)
                    .into_inner(),
            ),
        )
        .layer(middleware_stack)
        // TODO!!!
        .layer(
@@ -153,7 +88,11 @@
                .allow_origin(Any)
                .allow_credentials(false),
        )
        .layer(AddExtensionLayer::new(pool));
        .layer(AddExtensionLayer::new(pool))
        .layer(AddExtensionLayer::new(Arc::new(
            config.create_oidc_clients().await.unwrap(),
        )))
        .layer(AddExtensionLayer::new(Arc::new(config)));

    axum::Server::bind(&"0.0.0.0:8888".parse().unwrap())
        .serve(app.into_make_service_with_connect_info::<std::net::SocketAddr, _>())
diff --git a/book/src/getting-started/installation.md b/book/src/getting-started/installation.md
index c1c3efc..8d7d77c 100644
--- a/book/src/getting-started/installation.md
+++ a/book/src/getting-started/installation.md
@@ -12,7 +12,8 @@

Each of these services are hosted separately from one another, and could technically be swapped
out for other implementations - the only shared layer between the three of them is database
storage for crate lookups and authentication credential vending.
storage for crate lookups and authentication credential vending. All of the services have the
ability to be clustered with no extra configuration.

### Backend Services

diff --git a/chartered-frontend/src/pages/Login.tsx b/chartered-frontend/src/pages/Login.tsx
index b1c4fdb..0a4eb38 100644
--- a/chartered-frontend/src/pages/Login.tsx
+++ a/chartered-frontend/src/pages/Login.tsx
@@ -1,7 +1,8 @@
import React = require("react");
import { useState, useEffect, useRef } from "react";

import { useAuth } from "../useAuth";
import { useUnauthenticatedRequest } from "../util";

export default function Login() {
  const auth = useAuth();
@@ -12,6 +13,8 @@
  const [loading, setLoading] = useState(false);
  const isMountedRef = useRef(null);

  const { response: oauthProviders } = useUnauthenticatedRequest({ endpoint: "login/oauth/providers" });

  useEffect(() => {
    isMountedRef.current = true;
    return () => (isMountedRef.current = false);
@@ -61,36 +64,38 @@
            </div>

            <form onSubmit={handleSubmit}>
              <div className="mb-3">
                <label htmlFor="username" className="form-label">
                  Username
                </label>
              <div className="form-floating">
                <input
                  type="text"
                  className="form-control"
                  placeholder="john.smith"
                  id="username"
                  disabled={loading}
                  value={username}
                  onChange={(e) => setUsername(e.target.value)}
                />

                <label htmlFor="email" className="form-label">Username</label>
              </div>
              <div className="mb-3">
                <label htmlFor="password" className="form-label">
                  Password
                </label>

              <div className="form-floating mt-2">
                <input
                  type="password"
                  className="form-control"
                  placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;"
                  id="password"
                  disabled={loading}
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                />

                <label htmlFor="password" className="form-label">Password</label>
              </div>
              <div className="ml-auto">

              <div className="mt-2 ml-auto">
                <button
                  type="submit"
                  className="btn btn-primary"
                  className="btn btn-lg btn-primary w-100"
                  style={{ display: !loading ? "block" : "none" }}
                >
                  Login
@@ -105,6 +110,12 @@
                </div>
              </div>
            </form>

            {oauthProviders?.providers.length > 0 ? (<>
              <div className="side-lines mt-3">or</div>

              {oauthProviders.providers.map((v, i) => <a href="#" key={i} className="btn btn-lg btn-dark w-100 mt-3">Login with {v}</a>)}
            </>): <></>}
          </div>
        </div>
      </div>
diff --git a/chartered-web/src/endpoints/cargo_api/mod.rs b/chartered-web/src/endpoints/cargo_api/mod.rs
index 9ea8e43..1740db0 100644
--- a/chartered-web/src/endpoints/cargo_api/mod.rs
+++ a/chartered-web/src/endpoints/cargo_api/mod.rs
@@ -1,10 +1,33 @@
mod download;
mod owners;
mod publish;
mod yank;

pub use download::handle as download;
pub use owners::handle_get as get_owners;
pub use publish::handle as publish;
pub use yank::handle_unyank as unyank;
pub use yank::handle_yank as yank;
use axum::{
    body::{Body, BoxBody},
    handler::{delete, get, put},
    http::{Request, Response},
    Router,
};
use futures::future::Future;
use std::convert::Infallible;

pub fn routes() -> Router<
    impl tower::Service<
            Request<Body>,
            Response = Response<BoxBody>,
            Error = Infallible,
            Future = impl Future<Output = Result<Response<BoxBody>, Infallible>> + Send,
        > + Clone
        + Send,
> {
    crate::axum_box_after_every_route!(Router::new()
        .route("/crates/new", put(publish::handle))
        // .route("/crates/search", get(hello_world))
        .route("/crates/:crate/owners", get(owners::handle_get))
        // .route("/crates/:crate/owners", put(hello_world))
        // .route("/crates/:crate/owners", delete(hello_world))
        .route("/crates/:crate/:version/yank", delete(yank::handle_yank))
        .route("/crates/:crate/:version/unyank", put(yank::handle_unyank))
        .route("/crates/:crate/:version/download", get(download::handle)))
}
diff --git a/chartered-web/src/endpoints/web_api/login.rs b/chartered-web/src/endpoints/web_api/login.rs
deleted file mode 100644
index a617bb8..0000000 100644
--- a/chartered-web/src/endpoints/web_api/login.rs
+++ /dev/null
@@ -1,75 +1,0 @@
use axum::{extract, Json};
use chartered_db::{
    users::{User, UserSession},
    ConnectionPool,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("Failed to query database")]
    Database(#[from] chartered_db::Error),
    #[error("Invalid username/password")]
    UnknownUser,
}

impl Error {
    pub fn status_code(&self) -> axum::http::StatusCode {
        use axum::http::StatusCode;

        match self {
            Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
            Self::UnknownUser => StatusCode::FORBIDDEN,
        }
    }
}

define_error_response!(Error);

pub async fn handle(
    extract::Extension(db): extract::Extension<ConnectionPool>,
    extract::Json(req): extract::Json<Request>,
    user_agent: Option<extract::TypedHeader<headers::UserAgent>>,
    extract::ConnectInfo(addr): extract::ConnectInfo<std::net::SocketAddr>,
) -> Result<Json<Response>, Error> {
    // TODO: passwords
    let user = User::find_by_username(db.clone(), req.username)
        .await?
        .ok_or(Error::UnknownUser)?;

    let user_agent = if let Some(extract::TypedHeader(user_agent)) = user_agent {
        Some(user_agent.as_str().to_string())
    } else {
        None
    };

    // todo: session? ip storage? etc...
    let expires = chrono::Utc::now() + chrono::Duration::hours(1);
    let key = UserSession::generate(
        db,
        user.id,
        None,
        Some(expires.naive_utc()),
        user_agent,
        Some(addr.to_string()),
    )
    .await?;

    Ok(Json(Response {
        key: key.session_key,
        expires,
    }))
}

#[derive(Deserialize)]
pub struct Request {
    username: String,
    password: String,
}

#[derive(Serialize)]
pub struct Response {
    key: String,
    expires: chrono::DateTime<chrono::Utc>,
}
diff --git a/chartered-web/src/endpoints/web_api/mod.rs b/chartered-web/src/endpoints/web_api/mod.rs
index d44e5e5..5e80e62 100644
--- a/chartered-web/src/endpoints/web_api/mod.rs
+++ a/chartered-web/src/endpoints/web_api/mod.rs
@@ -1,11 +1,44 @@
pub mod crates;
mod login;
pub mod organisations;
mod auth;
mod crates;
mod organisations;
mod search_users;
mod ssh_key;

pub use login::handle as login;
pub use search_users::handle as search_users;
pub use ssh_key::{
    handle_delete as delete_ssh_key, handle_get as get_ssh_keys, handle_put as add_ssh_key,
use axum::{
    body::{Body, BoxBody},
    handler::{delete, get, put},
    http::{Request, Response},
    Router,
};
use futures::future::Future;
use std::convert::Infallible;

pub fn authenticated_routes() -> Router<
    impl tower::Service<
            Request<Body>,
            Response = Response<BoxBody>,
            Error = Infallible,
            Future = impl Future<Output = Result<Response<BoxBody>, Infallible>> + Send,
        > + Clone
        + Send,
> {
    crate::axum_box_after_every_route!(Router::new()
        .nest("/organisations", organisations::routes())
        .nest("/crates", crates::routes())
        .route("/users/search", get(search_users::handle))
        .route("/ssh-key", get(ssh_key::handle_get))
        .route("/ssh-key", put(ssh_key::handle_put))
        .route("/ssh-key/:id", delete(ssh_key::handle_delete)))
}

pub fn unauthenticated_routes() -> Router<
    impl tower::Service<
            Request<Body>,
            Response = Response<BoxBody>,
            Error = Infallible,
            Future = impl Future<Output = Result<Response<BoxBody>, Infallible>> + Send,
        > + Clone
        + Send,
> {
    crate::axum_box_after_every_route!(Router::new().nest("/login", auth::routes()))
}
diff --git a/chartered-web/src/endpoints/web_api/auth/mod.rs b/chartered-web/src/endpoints/web_api/auth/mod.rs
new file mode 100644
index 0000000..e0b309d 100644
--- /dev/null
+++ a/chartered-web/src/endpoints/web_api/auth/mod.rs
@@ -1,0 +1,68 @@
use axum::{
    body::{Body, BoxBody},
    extract,
    handler::{get, post},
    http::{Request, Response},
    Router,
};
use chartered_db::{
    users::{User, UserSession},
    ConnectionPool,
};
use futures::future::Future;
use serde::Serialize;
use std::convert::Infallible;

pub mod openid;
pub mod password;

pub fn routes() -> Router<
    impl tower::Service<
            Request<Body>,
            Response = Response<BoxBody>,
            Error = Infallible,
            Future = impl Future<Output = Result<Response<BoxBody>, Infallible>> + Send,
        > + Clone
        + Send,
> {
    crate::axum_box_after_every_route!(Router::new()
        .route("/password", post(password::handle))
        .route("/oauth/:provider/begin", get(openid::begin_oidc))
        .route("/oauth/complete", get(openid::complete_oidc))
        .route("/oauth/providers", get(openid::list_providers)))
}

#[derive(Serialize)]
pub struct LoginResponse {
    key: String,
    expires: chrono::DateTime<chrono::Utc>,
}

pub async fn login(
    db: ConnectionPool,
    user: User,
    user_agent: Option<extract::TypedHeader<headers::UserAgent>>,
    extract::ConnectInfo(addr): extract::ConnectInfo<std::net::SocketAddr>,
) -> Result<LoginResponse, chartered_db::Error> {
    let user_agent = if let Some(extract::TypedHeader(user_agent)) = user_agent {
        Some(user_agent.as_str().to_string())
    } else {
        None
    };

    let expires = chrono::Utc::now() + chrono::Duration::hours(1);
    let key = UserSession::generate(
        db,
        user.id,
        None,
        Some(expires.naive_utc()),
        user_agent,
        Some(addr.to_string()),
    )
    .await?;

    Ok(LoginResponse {
        key: key.session_key,
        expires,
    })
}
diff --git a/chartered-web/src/endpoints/web_api/auth/openid.rs b/chartered-web/src/endpoints/web_api/auth/openid.rs
new file mode 100644
index 0000000..63dee0f 100644
--- /dev/null
+++ a/chartered-web/src/endpoints/web_api/auth/openid.rs
@@ -1,0 +1,168 @@
use crate::config::{Config, OidcClients};
use axum::{extract, Json};
use chacha20poly1305::{
    aead::{Aead, NewAead},
    ChaCha20Poly1305, Nonce as ChaCha20Poly1305Nonce,
};
use chartered_db::{users::User, ConnectionPool};
use openid::{Options, Token};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use thiserror::Error;

pub type Nonce = [u8; 16];

#[derive(Serialize)]
pub struct ListProvidersResponse {
    providers: Vec<String>,
}

pub async fn list_providers(
    extract::Extension(oidc_clients): extract::Extension<Arc<OidcClients>>,
) -> Json<ListProvidersResponse> {
    Json(ListProvidersResponse {
        providers: oidc_clients
            .keys()
            .into_iter()
            .map(|v| v.to_string())
            .collect(),
    })
}

#[derive(Serialize, Deserialize, Debug)]
pub struct State {
    provider: String,
    nonce: Nonce,
}

#[derive(Serialize)]
pub struct BeginResponse {
    redirect_url: String,
}

pub async fn begin_oidc(
    extract::Path(provider): extract::Path<String>,
    extract::Extension(config): extract::Extension<Arc<Config>>,
    extract::Extension(oidc_clients): extract::Extension<Arc<OidcClients>>,
) -> Result<Json<BeginResponse>, Error> {
    let client = oidc_clients
        .get(&provider)
        .ok_or(Error::UnknownOauthProvider)?;

    let nonce = rand::random::<Nonce>();
    let state = serde_json::to_vec(&State { provider, nonce })?;

    let auth_url = client.auth_url(&Options {
        scope: Some("openid email profile".into()),
        nonce: Some(base64::encode_config(&nonce, base64::URL_SAFE_NO_PAD)),
        state: Some(encrypt_url_safe(&state, &config)?),
        ..Default::default()
    });

    Ok(Json(BeginResponse {
        redirect_url: auth_url.to_string(),
    }))
}

#[derive(Deserialize)]
pub struct CompleteOidcParams {
    state: String,
    code: String,
    scope: Option<String>,
    prompt: Option<String>,
}

pub async fn complete_oidc(
    extract::Query(params): extract::Query<CompleteOidcParams>,
    extract::Extension(config): extract::Extension<Arc<Config>>,
    extract::Extension(oidc_clients): extract::Extension<Arc<OidcClients>>,
    extract::Extension(db): extract::Extension<ConnectionPool>,
    user_agent: Option<extract::TypedHeader<headers::UserAgent>>,
    addr: extract::ConnectInfo<std::net::SocketAddr>,
) -> Result<Json<super::LoginResponse>, Error> {
    let state: State = serde_json::from_slice(&decrypt_url_safe(&params.state, &config)?)?;

    let client = oidc_clients
        .get(&state.provider)
        .ok_or(Error::UnknownOauthProvider)?;

    let mut token: Token = client.request_token(&params.code).await?.into();

    if let Some(mut id_token) = token.id_token.as_mut() {
        client.decode_token(&mut id_token)?;

        let nonce = base64::encode_config(state.nonce, base64::URL_SAFE_NO_PAD);
        client.validate_token(&id_token, Some(nonce.as_str()), None)?;
    } else {
        return Err(Error::MissingToken);
    }

    let userinfo = client.request_userinfo(&token).await?;

    let user = User::find_or_create(
        db.clone(),
        format!("{}:{}", state.provider, userinfo.sub.unwrap()),
    )
    .await?;

    Ok(Json(super::login(db, user, user_agent, addr).await?))
}

const NONCE_LEN: usize = 12;

fn encrypt_url_safe(input: &[u8], config: &Config) -> Result<String, Error> {
    let cipher = ChaCha20Poly1305::new(&config.encryption_key);

    let nonce = rand::random::<[u8; NONCE_LEN]>();
    let nonce = ChaCha20Poly1305Nonce::from_slice(&nonce);

    let mut ciphertext = cipher.encrypt(nonce, input)?;
    ciphertext.extend_from_slice(&nonce);

    Ok(base64::encode_config(&ciphertext, base64::URL_SAFE_NO_PAD))
}

fn decrypt_url_safe(input: &str, config: &Config) -> Result<Vec<u8>, Error> {
    let cipher = ChaCha20Poly1305::new(&config.encryption_key);

    let mut ciphertext = base64::decode_config(input, base64::URL_SAFE_NO_PAD)?;
    let ciphertext_nonce = ciphertext.split_off(ciphertext.len() - NONCE_LEN);
    let ciphertext_nonce = ChaCha20Poly1305Nonce::from_slice(&ciphertext_nonce);

    cipher
        .decrypt(ciphertext_nonce, ciphertext.as_ref())
        .map_err(Error::from)
}

#[derive(Error, Debug)]
pub enum Error {
    #[error("{0}")]
    Database(#[from] chartered_db::Error),
    #[error("Error serialising/deserialsing state: {0}")]
    Serde(#[from] serde_json::Error),
    #[error("Unknown OAuth provider given")]
    UnknownOauthProvider,
    #[error("{0}")]
    OAuth(#[from] openid::error::Error),
    #[error("{0}")]
    OAuthClient(#[from] openid::error::ClientError),
    #[error("Error during encryption/decryption")]
    Cipher(#[from] chacha20poly1305::aead::Error),
    #[error("Base64 error")]
    Base64(#[from] base64::DecodeError),
    #[error("Missing id_token")]
    MissingToken,
}

impl Error {
    fn status_code(&self) -> axum::http::StatusCode {
        use axum::http::StatusCode;

        match self {
            Self::Database(e) => e.status_code(),
            _ => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

define_error_response!(Error);
diff --git a/chartered-web/src/endpoints/web_api/auth/password.rs b/chartered-web/src/endpoints/web_api/auth/password.rs
new file mode 100644
index 0000000..5e2b720 100644
--- /dev/null
+++ a/chartered-web/src/endpoints/web_api/auth/password.rs
@@ -1,0 +1,45 @@
use axum::{extract, Json};
use chartered_db::{users::User, ConnectionPool};
use serde::Deserialize;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("Failed to query database")]
    Database(#[from] chartered_db::Error),
    #[error("Invalid username/password")]
    UnknownUser,
}

impl Error {
    pub fn status_code(&self) -> axum::http::StatusCode {
        use axum::http::StatusCode;

        match self {
            Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
            Self::UnknownUser => StatusCode::FORBIDDEN,
        }
    }
}

define_error_response!(Error);

pub async fn handle(
    extract::Extension(db): extract::Extension<ConnectionPool>,
    extract::Json(req): extract::Json<Request>,
    user_agent: Option<extract::TypedHeader<headers::UserAgent>>,
    addr: extract::ConnectInfo<std::net::SocketAddr>,
) -> Result<Json<super::LoginResponse>, Error> {
    // TODO: passwords
    let user = User::find_by_username(db.clone(), req.username)
        .await?
        .ok_or(Error::UnknownUser)?;

    Ok(Json(super::login(db, user, user_agent, addr).await?))
}

#[derive(Deserialize)]
pub struct Request {
    username: String,
    password: String,
}
diff --git a/chartered-web/src/endpoints/web_api/crates/mod.rs b/chartered-web/src/endpoints/web_api/crates/mod.rs
index 3113603..5f6b636 100644
--- a/chartered-web/src/endpoints/web_api/crates/mod.rs
+++ a/chartered-web/src/endpoints/web_api/crates/mod.rs
@@ -1,10 +1,30 @@
mod info;
mod members;
mod recently_updated;

pub use info::handle as info;
pub use members::{
    handle_delete as delete_member, handle_get as get_members, handle_patch as update_member,
    handle_put as insert_member,
use axum::{
    body::{Body, BoxBody},
    handler::{delete, get, patch, put},
    http::{Request, Response},
    Router,
};
pub use recently_updated::handle as list_recently_updated;
use futures::future::Future;
use std::convert::Infallible;

pub fn routes() -> Router<
    impl tower::Service<
            Request<Body>,
            Response = Response<BoxBody>,
            Error = Infallible,
            Future = impl Future<Output = Result<Response<BoxBody>, Infallible>> + Send,
        > + Clone
        + Send,
> {
    crate::axum_box_after_every_route!(Router::new()
        .route("/:org/:crate", get(info::handle))
        .route("/:org/:crate/members", get(members::handle_get))
        .route("/:org/:crate/members", patch(members::handle_patch))
        .route("/:org/:crate/members", put(members::handle_put))
        .route("/:org/:crate/members", delete(members::handle_delete))
        .route("/recently-updated", get(recently_updated::handle)))
}
diff --git a/chartered-web/src/endpoints/web_api/organisations/mod.rs b/chartered-web/src/endpoints/web_api/organisations/mod.rs
index 7ddfc99..525b1ff 100644
--- a/chartered-web/src/endpoints/web_api/organisations/mod.rs
+++ a/chartered-web/src/endpoints/web_api/organisations/mod.rs
@@ -1,11 +1,31 @@
mod crud;
mod info;
mod list;
mod members;

pub use crud::handle_put as create;
pub use info::handle_get as info;
pub use list::handle_get as list;
pub use members::{
    handle_delete as delete_member, handle_patch as update_member, handle_put as insert_member,
use axum::{
    body::{Body, BoxBody},
    handler::{delete, get, patch, put},
    http::{Request, Response},
    Router,
};
use futures::future::Future;
use std::convert::Infallible;

pub fn routes() -> Router<
    impl tower::Service<
            Request<Body>,
            Response = Response<BoxBody>,
            Error = Infallible,
            Future = impl Future<Output = Result<Response<BoxBody>, Infallible>> + Send,
        > + Clone
        + Send,
> {
    crate::axum_box_after_every_route!(Router::new()
        .route("/", get(list::handle_get))
        .route("/", put(crud::handle_put))
        .route("/:org", get(info::handle_get))
        .route("/:org/members", patch(members::handle_patch))
        .route("/:org/members", put(members::handle_put))
        .route("/:org/members", delete(members::handle_delete)))
}