summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.sqlx/query-1946af14f5d3da9af51fc0e3d4f25cff1556aec7083bc484172c58cbd655a316.json (renamed from .sqlx/query-693b6758b8e8a21f8e8017573526a2ef050389298851a2c0da9adec7d05fc862.json)4
-rw-r--r--.sqlx/query-558491955176ee2f12d80ce11deb37b5fea52bc9ac224957eb4f892aeaf84eaa.json12
-rw-r--r--.sqlx/query-584aea21a5ceb0ce6e48bc224bfc431f2e517755983f3510565e18ecb0e6e637.json (renamed from .sqlx/query-e06cbe45408d593a081b566c38657da87439340801d0765f8d9f64f8fbbbfb6b.json)4
-rw-r--r--.sqlx/query-659c2537dfa447799709d06f2da65c19b0ec4f1eac4d81756b0c9368b54fc24c.json (renamed from .sqlx/query-d704486ba77fb40e4dd40ff96acc7783f88618a18797958d558ef6687dc29750.json)4
-rw-r--r--.sqlx/query-736bf4e90f23163d7d3a551d5a848b53bc8febe8d4417b5162f8ca330daac885.json12
-rw-r--r--.sqlx/query-d49d4ab5f1bf4c78fa619680b04a506cd63a85741923f841b7c36c46b70a538f.json (renamed from .sqlx/query-ec1bfce459faaf3d4adbed040105e9e7fa30a6aac5580036d052a1270e13a311.json)4
-rw-r--r--Cargo.lock34
-rw-r--r--Cargo.toml3
-rw-r--r--build.rs45
-rwxr-xr-xgit-hooks/pre-commit4
-rw-r--r--src/boot/routes/get.rs9
-rw-r--r--src/boot/routes/test.rs6
-rw-r--r--src/channel/routes/channel/delete.rs4
-rw-r--r--src/channel/routes/channel/post.rs6
-rw-r--r--src/channel/routes/channel/test.rs52
-rw-r--r--src/channel/routes/post.rs4
-rw-r--r--src/channel/routes/test.rs6
-rw-r--r--src/event/routes/test.rs26
-rw-r--r--src/invite/repo.rs58
-rw-r--r--src/invite/routes/invite/post.rs6
-rw-r--r--src/invite/routes/post.rs10
-rw-r--r--src/login/extract.rs15
-rw-r--r--src/login/mod.rs1
-rw-r--r--src/login/routes/login/post.rs6
-rw-r--r--src/login/routes/login/test.rs29
-rw-r--r--src/login/routes/logout/post.rs6
-rw-r--r--src/login/routes/logout/test.rs14
-rw-r--r--src/message/app.rs2
-rw-r--r--src/message/routes/message.rs10
-rw-r--r--src/setup/routes/post.rs6
-rw-r--r--src/test/fixtures/cookie.rs37
-rw-r--r--src/test/fixtures/identity.rs48
-rw-r--r--src/test/fixtures/login.rs4
-rw-r--r--src/test/fixtures/mod.rs1
-rw-r--r--src/token/extract/cookie.rs (renamed from src/token/extract/identity_token.rs)22
-rw-r--r--src/token/extract/identity.rs6
-rw-r--r--src/token/extract/mod.rs4
-rw-r--r--src/ui/assets.rs32
-rw-r--r--src/ui/mime.rs22
-rw-r--r--src/ui/mod.rs1
-rw-r--r--src/ui/routes/ch/channel.rs6
-rw-r--r--src/ui/routes/get.rs6
-rw-r--r--src/ui/routes/path.rs7
-rwxr-xr-xtools/run1
44 files changed, 361 insertions, 238 deletions
diff --git a/.sqlx/query-693b6758b8e8a21f8e8017573526a2ef050389298851a2c0da9adec7d05fc862.json b/.sqlx/query-1946af14f5d3da9af51fc0e3d4f25cff1556aec7083bc484172c58cbd655a316.json
index c0791f7..f765fda 100644
--- a/.sqlx/query-693b6758b8e8a21f8e8017573526a2ef050389298851a2c0da9adec7d05fc862.json
+++ b/.sqlx/query-1946af14f5d3da9af51fc0e3d4f25cff1556aec7083bc484172c58cbd655a316.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n\t\t\t\tinsert into invite (id, issuer, issued_at)\n\t\t\t\tvalues ($1, $2, $3)\n\t\t\t\treturning\n\t\t\t\t\tid as \"id: Id\",\n\t\t\t\t\tissuer as \"issuer: login::Id\",\n\t\t\t\t\tissued_at as \"issued_at: DateTime\"\n\t\t\t",
+ "query": "\n insert into invite (id, issuer, issued_at)\n values ($1, $2, $3)\n returning\n id as \"id: Id\",\n issuer as \"issuer: login::Id\",\n issued_at as \"issued_at: DateTime\"\n ",
"describe": {
"columns": [
{
@@ -28,5 +28,5 @@
false
]
},
- "hash": "693b6758b8e8a21f8e8017573526a2ef050389298851a2c0da9adec7d05fc862"
+ "hash": "1946af14f5d3da9af51fc0e3d4f25cff1556aec7083bc484172c58cbd655a316"
}
diff --git a/.sqlx/query-558491955176ee2f12d80ce11deb37b5fea52bc9ac224957eb4f892aeaf84eaa.json b/.sqlx/query-558491955176ee2f12d80ce11deb37b5fea52bc9ac224957eb4f892aeaf84eaa.json
new file mode 100644
index 0000000..5f21bf5
--- /dev/null
+++ b/.sqlx/query-558491955176ee2f12d80ce11deb37b5fea52bc9ac224957eb4f892aeaf84eaa.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n delete from invite\n where issued_at < $1\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "558491955176ee2f12d80ce11deb37b5fea52bc9ac224957eb4f892aeaf84eaa"
+}
diff --git a/.sqlx/query-e06cbe45408d593a081b566c38657da87439340801d0765f8d9f64f8fbbbfb6b.json b/.sqlx/query-584aea21a5ceb0ce6e48bc224bfc431f2e517755983f3510565e18ecb0e6e637.json
index 662e8c2..f443d9a 100644
--- a/.sqlx/query-e06cbe45408d593a081b566c38657da87439340801d0765f8d9f64f8fbbbfb6b.json
+++ b/.sqlx/query-584aea21a5ceb0ce6e48bc224bfc431f2e517755983f3510565e18ecb0e6e637.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n\t\t\t\tselect\n invite.id as \"invite_id: Id\",\n\t\t\t\t\tissuer.id as \"issuer_id: login::Id\",\n\t\t\t\t\tissuer.display_name as \"issuer_name: nfc::String\",\n\t\t\t\t\tinvite.issued_at as \"invite_issued_at: DateTime\"\n\t\t\t\tfrom invite\n\t\t\t\tjoin login as issuer on (invite.issuer = issuer.id)\n\t\t\t\twhere invite.id = $1\n\t\t\t",
+ "query": "\n select\n invite.id as \"invite_id: Id\",\n issuer.id as \"issuer_id: login::Id\",\n issuer.display_name as \"issuer_name: nfc::String\",\n invite.issued_at as \"invite_issued_at: DateTime\"\n from invite\n join login as issuer on (invite.issuer = issuer.id)\n where invite.id = $1\n ",
"describe": {
"columns": [
{
@@ -34,5 +34,5 @@
false
]
},
- "hash": "e06cbe45408d593a081b566c38657da87439340801d0765f8d9f64f8fbbbfb6b"
+ "hash": "584aea21a5ceb0ce6e48bc224bfc431f2e517755983f3510565e18ecb0e6e637"
}
diff --git a/.sqlx/query-d704486ba77fb40e4dd40ff96acc7783f88618a18797958d558ef6687dc29750.json b/.sqlx/query-659c2537dfa447799709d06f2da65c19b0ec4f1eac4d81756b0c9368b54fc24c.json
index 2004e8f..232a15e 100644
--- a/.sqlx/query-d704486ba77fb40e4dd40ff96acc7783f88618a18797958d558ef6687dc29750.json
+++ b/.sqlx/query-659c2537dfa447799709d06f2da65c19b0ec4f1eac4d81756b0c9368b54fc24c.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n\t\t\t\tdelete from invite\n\t\t\t\twhere id = $1\n\t\t\t\treturning 1 as \"deleted: bool\"\n\t\t\t",
+ "query": "\n delete from invite\n where id = $1\n returning 1 as \"deleted: bool\"\n ",
"describe": {
"columns": [
{
@@ -16,5 +16,5 @@
null
]
},
- "hash": "d704486ba77fb40e4dd40ff96acc7783f88618a18797958d558ef6687dc29750"
+ "hash": "659c2537dfa447799709d06f2da65c19b0ec4f1eac4d81756b0c9368b54fc24c"
}
diff --git a/.sqlx/query-736bf4e90f23163d7d3a551d5a848b53bc8febe8d4417b5162f8ca330daac885.json b/.sqlx/query-736bf4e90f23163d7d3a551d5a848b53bc8febe8d4417b5162f8ca330daac885.json
deleted file mode 100644
index f7c0552..0000000
--- a/.sqlx/query-736bf4e90f23163d7d3a551d5a848b53bc8febe8d4417b5162f8ca330daac885.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n\t\t\t\tdelete from invite\n\t\t\t\twhere issued_at < $1\n\t\t\t",
- "describe": {
- "columns": [],
- "parameters": {
- "Right": 1
- },
- "nullable": []
- },
- "hash": "736bf4e90f23163d7d3a551d5a848b53bc8febe8d4417b5162f8ca330daac885"
-}
diff --git a/.sqlx/query-ec1bfce459faaf3d4adbed040105e9e7fa30a6aac5580036d052a1270e13a311.json b/.sqlx/query-d49d4ab5f1bf4c78fa619680b04a506cd63a85741923f841b7c36c46b70a538f.json
index f38401c..3ec71e8 100644
--- a/.sqlx/query-ec1bfce459faaf3d4adbed040105e9e7fa30a6aac5580036d052a1270e13a311.json
+++ b/.sqlx/query-d49d4ab5f1bf4c78fa619680b04a506cd63a85741923f841b7c36c46b70a538f.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n\t\t\t\tselect\n\t\t\t\t\tid as \"id: Id\",\n\t\t\t\t\tissuer as \"issuer: login::Id\",\n\t\t\t\t\tissued_at as \"issued_at: DateTime\"\n\t\t\t\tfrom invite\n\t\t\t\twhere id = $1\n\t\t\t",
+ "query": "\n select\n id as \"id: Id\",\n issuer as \"issuer: login::Id\",\n issued_at as \"issued_at: DateTime\"\n from invite\n where id = $1\n ",
"describe": {
"columns": [
{
@@ -28,5 +28,5 @@
false
]
},
- "hash": "ec1bfce459faaf3d4adbed040105e9e7fa30a6aac5580036d052a1270e13a311"
+ "hash": "d49d4ab5f1bf4c78fa619680b04a506cd63a85741923f841b7c36c46b70a538f"
}
diff --git a/Cargo.lock b/Cargo.lock
index 1cb656d..2b8eb5e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -813,7 +813,7 @@ dependencies = [
"headers",
"hex-literal",
"itertools",
- "mime_guess",
+ "mime",
"password-hash",
"rand",
"rand_core",
@@ -827,6 +827,7 @@ dependencies = [
"tokio-stream",
"unicode-casefold",
"unicode-normalization",
+ "unix_path",
"uuid",
]
@@ -1094,16 +1095,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
-name = "mime_guess"
-version = "2.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
-dependencies = [
- "mime",
- "unicase",
-]
-
-[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2098,12 +2089,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
-name = "unicase"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
-
-[[package]]
name = "unicode-bidi"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2143,6 +2128,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
+name = "unix_path"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af8e291873ae77c4c8d9c9b34d0bee68a35b048fb39c263a5155e0e353783eaf"
+dependencies = [
+ "unix_str",
+]
+
+[[package]]
+name = "unix_str"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ace0b4755d0a2959962769239d56267f8a024fef2d9b32666b3dcd0946b0906"
+
+[[package]]
name = "url"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 101498c..e9c9616 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,7 +16,7 @@ futures = "0.3.31"
headers = "0.4.0"
hex-literal = "0.4.1"
itertools = "0.13.0"
-mime_guess = "2.0.5"
+mime = "0.3.17"
password-hash = { version = "0.5.0", features = ["std"] }
rand = "0.8.5"
rand_core = { version = "0.6.4", features = ["getrandom"] }
@@ -34,6 +34,7 @@ tokio = { version = "1.40.0", features = ["rt", "macros", "rt-multi-thread"] }
tokio-stream = { version = "0.1.16", features = ["sync"] }
unicode-casefold = "0.2.0"
unicode-normalization = "0.1.24"
+unix_path = "1.0.1"
uuid = { version = "1.11.0", features = ["v4"] }
[dev-dependencies]
diff --git a/build.rs b/build.rs
index d506869..a61b2db 100644
--- a/build.rs
+++ b/build.rs
@@ -1,5 +1,44 @@
-// generated by `sqlx migrate build-script`
-fn main() {
+use std::{io, process::Command};
+
+fn main() -> Result<(), io::Error> {
// trigger recompilation when a new migration is added
- println!("cargo:rerun-if-changed=migrations");
+ println!("cargo::rerun-if-changed=migrations");
+
+ // rerun npm install whenever packages or npm config are changed
+ println!("cargo::rerun-if-changed=.npmrc");
+ println!("cargo::rerun-if-changed=package.json");
+ // `node_modules` and `package-lock.json` are always touched if `npm install`
+ // runs, leading to spurious rebuilds.
+ //
+ // See: <https://github.com/npm/cli/issues/7874>
+ // println!("cargo::rerun-if-changed=package-lock.json");
+ // println!("cargo::rerun-if-changed=node_modules");
+ let status = Command::new("npm").args(["install"]).status()?;
+ if !status.success() {
+ return Err(io::Error::other(format!(
+ "'npm install' exited with status {status:?}"
+ )));
+ }
+
+ // rerun `npm run build` whenever the UI changes.
+ //
+ // `node_modules` is always touched if `npm install` runs, leading to spurious
+ // rebuilds. (This duplicate is purely organizational; it reflects that the ui
+ // depends on node_modules.)
+ //
+ // See: <https://github.com/npm/cli/issues/7874>
+ // println!("cargo::rerun-if-changed=node_modules");
+ println!("cargo::rerun-if-changed=postcss.config.js");
+ println!("cargo::rerun-if-changed=svelte.config.js");
+ println!("cargo::rerun-if-changed=tailwind.config.js");
+ println!("cargo::rerun-if-changed=vite.config.js");
+ println!("cargo::rerun-if-changed=ui");
+ let status = Command::new("npm").args(["run", "build"]).status()?;
+ if !status.success() {
+ return Err(io::Error::other(format!(
+ "'npm run build' exited with status {status:?}"
+ )));
+ }
+
+ Ok(())
}
diff --git a/git-hooks/pre-commit b/git-hooks/pre-commit
index cafdb8d..4485005 100755
--- a/git-hooks/pre-commit
+++ b/git-hooks/pre-commit
@@ -4,8 +4,8 @@
# run. It gets old fast. That's why this uses `cargo check` and not `cargo
# test`, for example.
-# Make sure the UI is up to date.
-tools/build-ui
+# Make sure package-lock.json is up to date with package.json
+npm ci --dry-run
# Make sure Cargo.lock is up to date with Cargo.toml.
cargo update --locked --workspace
# Make sure there are no screamers in the code.
diff --git a/src/boot/routes/get.rs b/src/boot/routes/get.rs
index 737b479..563fbf1 100644
--- a/src/boot/routes/get.rs
+++ b/src/boot/routes/get.rs
@@ -3,11 +3,14 @@ use axum::{
response::{self, IntoResponse},
};
-use crate::{app::App, boot::Snapshot, error::Internal, login::Login};
+use crate::{app::App, boot::Snapshot, error::Internal, login::Login, token::extract::Identity};
-pub async fn handler(State(app): State<App>, login: Login) -> Result<Response, Internal> {
+pub async fn handler(State(app): State<App>, identity: Identity) -> Result<Response, Internal> {
let snapshot = app.boot().snapshot().await?;
- Ok(Response { login, snapshot })
+ Ok(Response {
+ login: identity.login,
+ snapshot,
+ })
}
#[derive(serde::Serialize)]
diff --git a/src/boot/routes/test.rs b/src/boot/routes/test.rs
index 4023753..0430854 100644
--- a/src/boot/routes/test.rs
+++ b/src/boot/routes/test.rs
@@ -6,10 +6,10 @@ use crate::test::fixtures;
#[tokio::test]
async fn returns_identity() {
let app = fixtures::scratch_app().await;
- let login = fixtures::login::fictitious();
- let response = get::handler(State(app), login.clone())
+ let identity = fixtures::identity::fictitious();
+ let response = get::handler(State(app), identity.clone())
.await
.expect("boot always succeeds");
- assert_eq!(login, response.login);
+ assert_eq!(identity.login, response.login);
}
diff --git a/src/channel/routes/channel/delete.rs b/src/channel/routes/channel/delete.rs
index 5f40ddf..91eb506 100644
--- a/src/channel/routes/channel/delete.rs
+++ b/src/channel/routes/channel/delete.rs
@@ -9,14 +9,14 @@ use crate::{
channel::app,
clock::RequestedAt,
error::{Internal, NotFound},
- login::Login,
+ token::extract::Identity,
};
pub async fn handler(
State(app): State<App>,
Path(channel): Path<super::PathInfo>,
RequestedAt(deleted_at): RequestedAt,
- _: Login,
+ _: Identity,
) -> Result<StatusCode, Error> {
app.channels().delete(&channel, &deleted_at).await?;
diff --git a/src/channel/routes/channel/post.rs b/src/channel/routes/channel/post.rs
index d0cae05..b51e691 100644
--- a/src/channel/routes/channel/post.rs
+++ b/src/channel/routes/channel/post.rs
@@ -8,20 +8,20 @@ use crate::{
app::App,
clock::RequestedAt,
error::{Internal, NotFound},
- login::Login,
message::{app::SendError, Body, Message},
+ token::extract::Identity,
};
pub async fn handler(
State(app): State<App>,
Path(channel): Path<super::PathInfo>,
RequestedAt(sent_at): RequestedAt,
- login: Login,
+ identity: Identity,
Json(request): Json<Request>,
) -> Result<Response, Error> {
let message = app
.messages()
- .send(&channel, &login, &sent_at, &request.body)
+ .send(&channel, &identity.login, &sent_at, &request.body)
.await?;
Ok(Response(message))
diff --git a/src/channel/routes/channel/test.rs b/src/channel/routes/channel/test.rs
index b895b69..9a2227d 100644
--- a/src/channel/routes/channel/test.rs
+++ b/src/channel/routes/channel/test.rs
@@ -14,7 +14,7 @@ async fn messages_in_order() {
// Set up the environment
let app = fixtures::scratch_app().await;
- let sender = fixtures::login::create(&app, &fixtures::now()).await;
+ let sender = fixtures::identity::create(&app, &fixtures::now()).await;
let channel = fixtures::channel::create(&app, &fixtures::now()).await;
// Call the endpoint (twice)
@@ -35,7 +35,7 @@ async fn messages_in_order() {
Json(request),
)
.await
- .expect("sending to a valid channel");
+ .expect("sending to a valid channel succeeds");
}
// Verify the semantics
@@ -44,7 +44,7 @@ async fn messages_in_order() {
.events()
.subscribe(None)
.await
- .expect("subscribing to a valid channel")
+ .expect("subscribing to a valid channel succeeds")
.filter_map(fixtures::message::events)
.take(requests.len());
@@ -55,7 +55,7 @@ async fn messages_in_order() {
assert!(matches!(
event,
message::Event::Sent(event)
- if event.message.sender == sender.id
+ if event.message.sender == sender.login.id
&& event.message.body == message
));
}
@@ -66,7 +66,7 @@ async fn nonexistent_channel() {
// Set up the environment
let app = fixtures::scratch_app().await;
- let login = fixtures::login::create(&app, &fixtures::now()).await;
+ let sender = fixtures::identity::create(&app, &fixtures::now()).await;
// Call the endpoint
@@ -79,11 +79,49 @@ async fn nonexistent_channel() {
State(app),
Path(channel.clone()),
sent_at,
- login,
+ sender,
Json(request),
)
.await
- .expect_err("sending to a nonexistent channel");
+ .expect_err("sending to a nonexistent channel fails");
+
+ // Verify the structure of the response
+
+ assert!(matches!(
+ error,
+ SendError::ChannelNotFound(error_channel) if channel == error_channel
+ ));
+}
+
+#[tokio::test]
+async fn deleted_channel() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let sender = fixtures::identity::create(&app, &fixtures::now()).await;
+ let channel = fixtures::channel::create(&app, &fixtures::now()).await;
+
+ app.channels()
+ .delete(&channel.id, &fixtures::now())
+ .await
+ .expect("deleting a new channel succeeds");
+
+ // Call the endpoint
+
+ let sent_at = fixtures::now();
+ let channel = channel::Id::generate();
+ let request = post::Request {
+ body: fixtures::message::propose(),
+ };
+ let post::Error(error) = post::handler(
+ State(app),
+ Path(channel.clone()),
+ sent_at,
+ sender,
+ Json(request),
+ )
+ .await
+ .expect_err("sending to a deleted channel fails");
// Verify the structure of the response
diff --git a/src/channel/routes/post.rs b/src/channel/routes/post.rs
index 9781dd7..810445c 100644
--- a/src/channel/routes/post.rs
+++ b/src/channel/routes/post.rs
@@ -9,13 +9,13 @@ use crate::{
channel::{app, Channel},
clock::RequestedAt,
error::Internal,
- login::Login,
name::Name,
+ token::extract::Identity,
};
pub async fn handler(
State(app): State<App>,
- _: Login, // requires auth, but doesn't actually care who you are
+ _: Identity, // requires auth, but doesn't actually care who you are
RequestedAt(created_at): RequestedAt,
Json(request): Json<Request>,
) -> Result<Response, Error> {
diff --git a/src/channel/routes/test.rs b/src/channel/routes/test.rs
index 7879ba0..77f283b 100644
--- a/src/channel/routes/test.rs
+++ b/src/channel/routes/test.rs
@@ -14,7 +14,7 @@ async fn new_channel() {
// Set up the environment
let app = fixtures::scratch_app().await;
- let creator = fixtures::login::create(&app, &fixtures::now()).await;
+ let creator = fixtures::identity::create(&app, &fixtures::now()).await;
// Call the endpoint
@@ -65,7 +65,7 @@ async fn duplicate_name() {
// Set up the environment
let app = fixtures::scratch_app().await;
- let creator = fixtures::login::create(&app, &fixtures::now()).await;
+ let creator = fixtures::identity::create(&app, &fixtures::now()).await;
let channel = fixtures::channel::create(&app, &fixtures::now()).await;
// Call the endpoint
@@ -91,7 +91,7 @@ async fn name_reusable_after_delete() {
// Set up the environment
let app = fixtures::scratch_app().await;
- let creator = fixtures::login::create(&app, &fixtures::now()).await;
+ let creator = fixtures::identity::create(&app, &fixtures::now()).await;
let name = fixtures::channel::propose();
// Call the endpoint (first time)
diff --git a/src/event/routes/test.rs b/src/event/routes/test.rs
index e6a8b9d..49f8094 100644
--- a/src/event/routes/test.rs
+++ b/src/event/routes/test.rs
@@ -22,8 +22,7 @@ async fn includes_historical_message() {
// Call the endpoint
- let subscriber_creds = fixtures::login::create_with_password(&app, &fixtures::now()).await;
- let subscriber = fixtures::identity::identity(&app, &subscriber_creds, &fixtures::now()).await;
+ let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
let get::Response(events) = get::handler(State(app), subscriber, None, Query::default())
.await
.expect("subscribe never fails");
@@ -49,8 +48,7 @@ async fn includes_live_message() {
// Call the endpoint
- let subscriber_creds = fixtures::login::create_with_password(&app, &fixtures::now()).await;
- let subscriber = fixtures::identity::identity(&app, &subscriber_creds, &fixtures::now()).await;
+ let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
let get::Response(events) =
get::handler(State(app.clone()), subscriber, None, Query::default())
.await
@@ -95,8 +93,7 @@ async fn includes_multiple_channels() {
// Call the endpoint
- let subscriber_creds = fixtures::login::create_with_password(&app, &fixtures::now()).await;
- let subscriber = fixtures::identity::identity(&app, &subscriber_creds, &fixtures::now()).await;
+ let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
let get::Response(events) = get::handler(State(app), subscriber, None, Query::default())
.await
.expect("subscribe never fails");
@@ -133,8 +130,7 @@ async fn sequential_messages() {
// Call the endpoint
- let subscriber_creds = fixtures::login::create_with_password(&app, &fixtures::now()).await;
- let subscriber = fixtures::identity::identity(&app, &subscriber_creds, &fixtures::now()).await;
+ let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
let get::Response(events) = get::handler(State(app), subscriber, None, Query::default())
.await
.expect("subscribe never fails");
@@ -180,8 +176,7 @@ async fn resumes_from() {
// Call the endpoint
- let subscriber_creds = fixtures::login::create_with_password(&app, &fixtures::now()).await;
- let subscriber = fixtures::identity::identity(&app, &subscriber_creds, &fixtures::now()).await;
+ let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
let resume_at = {
// First subscription
@@ -258,8 +253,7 @@ async fn serial_resume() {
// Call the endpoint
- let subscriber_creds = fixtures::login::create_with_password(&app, &fixtures::now()).await;
- let subscriber = fixtures::identity::identity(&app, &subscriber_creds, &fixtures::now()).await;
+ let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
let resume_at = {
let initial_messages = [
@@ -382,7 +376,7 @@ async fn terminates_on_token_expiry() {
let subscriber_creds = fixtures::login::create_with_password(&app, &fixtures::now()).await;
let subscriber =
- fixtures::identity::identity(&app, &subscriber_creds, &fixtures::ancient()).await;
+ fixtures::identity::logged_in(&app, &subscriber_creds, &fixtures::ancient()).await;
let get::Response(events) =
get::handler(State(app.clone()), subscriber, None, Query::default())
@@ -426,11 +420,7 @@ async fn terminates_on_logout() {
// Subscribe via the endpoint
- let subscriber_creds = fixtures::login::create_with_password(&app, &fixtures::now()).await;
- let subscriber_token =
- fixtures::identity::logged_in(&app, &subscriber_creds, &fixtures::now()).await;
- let subscriber =
- fixtures::identity::from_token(&app, &subscriber_token, &fixtures::now()).await;
+ let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
let get::Response(events) = get::handler(
State(app.clone()),
diff --git a/src/invite/repo.rs b/src/invite/repo.rs
index 02f4e42..5f86e49 100644
--- a/src/invite/repo.rs
+++ b/src/invite/repo.rs
@@ -29,13 +29,13 @@ impl<'c> Invites<'c> {
let invite = sqlx::query_as!(
Invite,
r#"
- insert into invite (id, issuer, issued_at)
- values ($1, $2, $3)
- returning
- id as "id: Id",
- issuer as "issuer: login::Id",
- issued_at as "issued_at: DateTime"
- "#,
+ insert into invite (id, issuer, issued_at)
+ values ($1, $2, $3)
+ returning
+ id as "id: Id",
+ issuer as "issuer: login::Id",
+ issued_at as "issued_at: DateTime"
+ "#,
id,
issuer.id,
issued_at
@@ -50,13 +50,13 @@ impl<'c> Invites<'c> {
let invite = sqlx::query_as!(
Invite,
r#"
- select
- id as "id: Id",
- issuer as "issuer: login::Id",
- issued_at as "issued_at: DateTime"
- from invite
- where id = $1
- "#,
+ select
+ id as "id: Id",
+ issuer as "issuer: login::Id",
+ issued_at as "issued_at: DateTime"
+ from invite
+ where id = $1
+ "#,
invite,
)
.fetch_one(&mut *self.0)
@@ -68,15 +68,15 @@ impl<'c> Invites<'c> {
pub async fn summary(&mut self, invite: &Id) -> Result<Summary, sqlx::Error> {
let invite = sqlx::query!(
r#"
- select
+ select
invite.id as "invite_id: Id",
- issuer.id as "issuer_id: login::Id",
- issuer.display_name as "issuer_name: nfc::String",
- invite.issued_at as "invite_issued_at: DateTime"
- from invite
- join login as issuer on (invite.issuer = issuer.id)
- where invite.id = $1
- "#,
+ issuer.id as "issuer_id: login::Id",
+ issuer.display_name as "issuer_name: nfc::String",
+ invite.issued_at as "invite_issued_at: DateTime"
+ from invite
+ join login as issuer on (invite.issuer = issuer.id)
+ where invite.id = $1
+ "#,
invite,
)
.map(|row| Summary {
@@ -93,10 +93,10 @@ impl<'c> Invites<'c> {
pub async fn accept(&mut self, invite: &Invite) -> Result<(), sqlx::Error> {
sqlx::query_scalar!(
r#"
- delete from invite
- where id = $1
- returning 1 as "deleted: bool"
- "#,
+ delete from invite
+ where id = $1
+ returning 1 as "deleted: bool"
+ "#,
invite.id,
)
.fetch_one(&mut *self.0)
@@ -108,9 +108,9 @@ impl<'c> Invites<'c> {
pub async fn expire(&mut self, expire_at: &DateTime) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
- delete from invite
- where issued_at < $1
- "#,
+ delete from invite
+ where issued_at < $1
+ "#,
expire_at,
)
.execute(&mut *self.0)
diff --git a/src/invite/routes/invite/post.rs b/src/invite/routes/invite/post.rs
index a41207a..3ca4e6b 100644
--- a/src/invite/routes/invite/post.rs
+++ b/src/invite/routes/invite/post.rs
@@ -11,16 +11,16 @@ use crate::{
invite::app,
login::{Login, Password},
name::Name,
- token::extract::IdentityToken,
+ token::extract::IdentityCookie,
};
pub async fn handler(
State(app): State<App>,
RequestedAt(accepted_at): RequestedAt,
- identity: IdentityToken,
+ identity: IdentityCookie,
Path(invite): Path<super::PathInfo>,
Json(request): Json<Request>,
-) -> Result<(IdentityToken, Json<Login>), Error> {
+) -> Result<(IdentityCookie, Json<Login>), Error> {
let (login, secret) = app
.invites()
.accept(&invite, &request.name, &request.password, &accepted_at)
diff --git a/src/invite/routes/post.rs b/src/invite/routes/post.rs
index 80b1c27..eb7d706 100644
--- a/src/invite/routes/post.rs
+++ b/src/invite/routes/post.rs
@@ -1,17 +1,19 @@
use axum::extract::{Json, State};
-use crate::{app::App, clock::RequestedAt, error::Internal, invite::Invite, login::Login};
+use crate::{
+ app::App, clock::RequestedAt, error::Internal, invite::Invite, token::extract::Identity,
+};
pub async fn handler(
State(app): State<App>,
RequestedAt(issued_at): RequestedAt,
- login: Login,
- // Require `{}` as the only valid request for this endpoint.
+ identity: Identity,
_: Json<Request>,
) -> Result<Json<Invite>, Internal> {
- let invite = app.invites().create(&login, &issued_at).await?;
+ let invite = app.invites().create(&identity.login, &issued_at).await?;
Ok(Json(invite))
}
+// Require `{}` as the only valid request for this endpoint.
#[derive(Default, serde::Deserialize)]
pub struct Request {}
diff --git a/src/login/extract.rs b/src/login/extract.rs
deleted file mode 100644
index c2d97f2..0000000
--- a/src/login/extract.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use axum::{extract::FromRequestParts, http::request::Parts};
-
-use super::Login;
-use crate::{app::App, token::extract::Identity};
-
-#[async_trait::async_trait]
-impl FromRequestParts<App> for Login {
- type Rejection = <Identity as FromRequestParts<App>>::Rejection;
-
- async fn from_request_parts(parts: &mut Parts, state: &App) -> Result<Self, Self::Rejection> {
- let identity = Identity::from_request_parts(parts, state).await?;
-
- Ok(identity.login)
- }
-}
diff --git a/src/login/mod.rs b/src/login/mod.rs
index 64a3698..279e9a6 100644
--- a/src/login/mod.rs
+++ b/src/login/mod.rs
@@ -1,6 +1,5 @@
pub mod app;
pub mod event;
-pub mod extract;
mod history;
mod id;
pub mod password;
diff --git a/src/login/routes/login/post.rs b/src/login/routes/login/post.rs
index 20430db..96da5c5 100644
--- a/src/login/routes/login/post.rs
+++ b/src/login/routes/login/post.rs
@@ -10,15 +10,15 @@ use crate::{
error::Internal,
login::{Login, Password},
name::Name,
- token::{app, extract::IdentityToken},
+ token::{app, extract::IdentityCookie},
};
pub async fn handler(
State(app): State<App>,
RequestedAt(now): RequestedAt,
- identity: IdentityToken,
+ identity: IdentityCookie,
Json(request): Json<Request>,
-) -> Result<(IdentityToken, Json<Login>), Error> {
+) -> Result<(IdentityCookie, Json<Login>), Error> {
let (login, secret) = app
.tokens()
.login(&request.name, &request.password, &now)
diff --git a/src/login/routes/login/test.rs b/src/login/routes/login/test.rs
index c94f14c..7399796 100644
--- a/src/login/routes/login/test.rs
+++ b/src/login/routes/login/test.rs
@@ -8,14 +8,14 @@ async fn correct_credentials() {
// Set up the environment
let app = fixtures::scratch_app().await;
- let (login, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await;
+ let (name, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await;
// Call the endpoint
- let identity = fixtures::identity::not_logged_in();
+ let identity = fixtures::cookie::not_logged_in();
let logged_in_at = fixtures::now();
let request = post::Request {
- name: login.name.clone(),
+ name: name.clone(),
password,
};
let (identity, Json(response)) =
@@ -25,8 +25,10 @@ async fn correct_credentials() {
// Verify the return value's basic structure
- assert_eq!(login, response);
- let secret = identity.secret().expect("logged in with valid credentials");
+ assert_eq!(name, response.name);
+ let secret = identity
+ .secret()
+ .expect("logged in with valid credentials issues an identity cookie");
// Verify the semantics
@@ -37,7 +39,7 @@ async fn correct_credentials() {
.await
.expect("identity secret is valid");
- assert_eq!(login, validated_login);
+ assert_eq!(response, validated_login);
}
#[tokio::test]
@@ -48,7 +50,7 @@ async fn invalid_name() {
// Call the endpoint
- let identity = fixtures::identity::not_logged_in();
+ let identity = fixtures::cookie::not_logged_in();
let logged_in_at = fixtures::now();
let (name, password) = fixtures::login::propose();
let request = post::Request {
@@ -58,7 +60,7 @@ async fn invalid_name() {
let post::Error(error) =
post::handler(State(app.clone()), logged_in_at, identity, Json(request))
.await
- .expect_err("logged in with an incorrect password");
+ .expect_err("logged in with an incorrect password fails");
// Verify the return value's basic structure
@@ -75,7 +77,7 @@ async fn incorrect_password() {
// Call the endpoint
let logged_in_at = fixtures::now();
- let identity = fixtures::identity::not_logged_in();
+ let identity = fixtures::cookie::not_logged_in();
let request = post::Request {
name: login.name,
password: fixtures::login::propose_password(),
@@ -95,16 +97,13 @@ async fn token_expires() {
// Set up the environment
let app = fixtures::scratch_app().await;
- let (login, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await;
+ let (name, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await;
// Call the endpoint
let logged_in_at = fixtures::ancient();
- let identity = fixtures::identity::not_logged_in();
- let request = post::Request {
- name: login.name.clone(),
- password,
- };
+ let identity = fixtures::cookie::not_logged_in();
+ let request = post::Request { name, password };
let (identity, _) = post::handler(State(app.clone()), logged_in_at, identity, Json(request))
.await
.expect("logged in with valid credentials");
diff --git a/src/login/routes/logout/post.rs b/src/login/routes/logout/post.rs
index 6b7a62a..bb09b9f 100644
--- a/src/login/routes/logout/post.rs
+++ b/src/login/routes/logout/post.rs
@@ -8,15 +8,15 @@ use crate::{
app::App,
clock::RequestedAt,
error::{Internal, Unauthorized},
- token::{app, extract::IdentityToken},
+ token::{app, extract::IdentityCookie},
};
pub async fn handler(
State(app): State<App>,
RequestedAt(now): RequestedAt,
- identity: IdentityToken,
+ identity: IdentityCookie,
Json(_): Json<Request>,
-) -> Result<(IdentityToken, StatusCode), Error> {
+) -> Result<(IdentityCookie, StatusCode), Error> {
if let Some(secret) = identity.secret() {
let (token, _) = app.tokens().validate(&secret, &now).await?;
app.tokens().logout(&token).await?;
diff --git a/src/login/routes/logout/test.rs b/src/login/routes/logout/test.rs
index 91837fe..775fa9f 100644
--- a/src/login/routes/logout/test.rs
+++ b/src/login/routes/logout/test.rs
@@ -12,9 +12,9 @@ async fn successful() {
let app = fixtures::scratch_app().await;
let now = fixtures::now();
- let login = fixtures::login::create_with_password(&app, &fixtures::now()).await;
- let identity = fixtures::identity::logged_in(&app, &login, &now).await;
- let secret = fixtures::identity::secret(&identity);
+ let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await;
+ let identity = fixtures::cookie::logged_in(&app, &creds, &now).await;
+ let secret = fixtures::cookie::secret(&identity);
// Call the endpoint
@@ -49,10 +49,10 @@ async fn no_identity() {
// Call the endpoint
- let identity = fixtures::identity::not_logged_in();
+ let identity = fixtures::cookie::not_logged_in();
let (identity, status) = post::handler(State(app), fixtures::now(), identity, Json::default())
.await
- .expect("logged out with no token");
+ .expect("logged out with no token succeeds");
// Verify the return value's basic structure
@@ -68,10 +68,10 @@ async fn invalid_token() {
// Call the endpoint
- let identity = fixtures::identity::fictitious();
+ let identity = fixtures::cookie::fictitious();
let post::Error(error) = post::handler(State(app), fixtures::now(), identity, Json::default())
.await
- .expect_err("logged out with an invalid token");
+ .expect_err("logged out with an invalid token fails");
// Verify the return value's basic structure
diff --git a/src/message/app.rs b/src/message/app.rs
index 852b958..eed6ba4 100644
--- a/src/message/app.rs
+++ b/src/message/app.rs
@@ -136,8 +136,6 @@ impl From<channel::repo::LoadError> for SendError {
#[derive(Debug, thiserror::Error)]
pub enum DeleteError {
- #[error("channel {0} not found")]
- ChannelNotFound(channel::Id),
#[error("message {0} not found")]
NotFound(Id),
#[error("message {0} deleted")]
diff --git a/src/message/routes/message.rs b/src/message/routes/message.rs
index fbef35a..f83cb39 100644
--- a/src/message/routes/message.rs
+++ b/src/message/routes/message.rs
@@ -9,15 +9,15 @@ pub mod delete {
app::App,
clock::RequestedAt,
error::{Internal, NotFound},
- login::Login,
message::{self, app::DeleteError},
+ token::extract::Identity,
};
pub async fn handler(
State(app): State<App>,
Path(message): Path<message::Id>,
RequestedAt(deleted_at): RequestedAt,
- _: Login,
+ _: Identity,
) -> Result<StatusCode, Error> {
app.messages().delete(&message, &deleted_at).await?;
@@ -33,9 +33,9 @@ pub mod delete {
let Self(error) = self;
#[allow(clippy::match_wildcard_for_single_variants)]
match error {
- DeleteError::ChannelNotFound(_)
- | DeleteError::NotFound(_)
- | DeleteError::Deleted(_) => NotFound(error).into_response(),
+ DeleteError::NotFound(_) | DeleteError::Deleted(_) => {
+ NotFound(error).into_response()
+ }
other => Internal::from(other).into_response(),
}
}
diff --git a/src/setup/routes/post.rs b/src/setup/routes/post.rs
index fb2280a..f7b256e 100644
--- a/src/setup/routes/post.rs
+++ b/src/setup/routes/post.rs
@@ -11,15 +11,15 @@ use crate::{
login::{Login, Password},
name::Name,
setup::app,
- token::extract::IdentityToken,
+ token::extract::IdentityCookie,
};
pub async fn handler(
State(app): State<App>,
RequestedAt(setup_at): RequestedAt,
- identity: IdentityToken,
+ identity: IdentityCookie,
Json(request): Json<Request>,
-) -> Result<(IdentityToken, Json<Login>), Error> {
+) -> Result<(IdentityCookie, Json<Login>), Error> {
let (login, secret) = app
.setup()
.initial(&request.name, &request.password, &setup_at)
diff --git a/src/test/fixtures/cookie.rs b/src/test/fixtures/cookie.rs
new file mode 100644
index 0000000..58777c8
--- /dev/null
+++ b/src/test/fixtures/cookie.rs
@@ -0,0 +1,37 @@
+use uuid::Uuid;
+
+use crate::{
+ app::App,
+ clock::RequestedAt,
+ login::Password,
+ name::Name,
+ token::{extract::IdentityCookie, Secret},
+};
+
+pub fn not_logged_in() -> IdentityCookie {
+ IdentityCookie::new()
+}
+
+pub async fn logged_in(
+ app: &App,
+ credentials: &(Name, Password),
+ now: &RequestedAt,
+) -> IdentityCookie {
+ let (name, password) = credentials;
+ let (_, token) = app
+ .tokens()
+ .login(name, password, now)
+ .await
+ .expect("should succeed given known-valid credentials");
+
+ IdentityCookie::new().set(token)
+}
+
+pub fn secret(identity: &IdentityCookie) -> Secret {
+ identity.secret().expect("identity contained a secret")
+}
+
+pub fn fictitious() -> IdentityCookie {
+ let token = Uuid::new_v4().to_string();
+ IdentityCookie::new().set(token)
+}
diff --git a/src/test/fixtures/identity.rs b/src/test/fixtures/identity.rs
index c434473..e438f2b 100644
--- a/src/test/fixtures/identity.rs
+++ b/src/test/fixtures/identity.rs
@@ -1,31 +1,21 @@
-use uuid::Uuid;
-
use crate::{
app::App,
clock::RequestedAt,
- login::{Login, Password},
+ login::Password,
+ name::Name,
+ test::fixtures,
token::{
- extract::{Identity, IdentityToken},
- Secret,
+ self,
+ extract::{Identity, IdentityCookie},
},
};
-pub fn not_logged_in() -> IdentityToken {
- IdentityToken::new()
-}
-
-pub async fn logged_in(app: &App, login: &(Login, Password), now: &RequestedAt) -> IdentityToken {
- let (login, password) = login;
- let (_, token) = app
- .tokens()
- .login(&login.name, password, now)
- .await
- .expect("should succeed given known-valid credentials");
-
- IdentityToken::new().set(token)
+pub async fn create(app: &App, created_at: &RequestedAt) -> Identity {
+ let credentials = fixtures::login::create_with_password(app, created_at).await;
+ logged_in(app, &credentials, created_at).await
}
-pub async fn from_token(app: &App, token: &IdentityToken, issued_at: &RequestedAt) -> Identity {
+pub async fn from_cookie(app: &App, token: &IdentityCookie, issued_at: &RequestedAt) -> Identity {
let secret = token.secret().expect("identity token has a secret");
let (token, login) = app
.tokens()
@@ -36,16 +26,18 @@ pub async fn from_token(app: &App, token: &IdentityToken, issued_at: &RequestedA
Identity { token, login }
}
-pub async fn identity(app: &App, login: &(Login, Password), issued_at: &RequestedAt) -> Identity {
- let secret = logged_in(app, login, issued_at).await;
- from_token(app, &secret, issued_at).await
+pub async fn logged_in(
+ app: &App,
+ credentials: &(Name, Password),
+ issued_at: &RequestedAt,
+) -> Identity {
+ let secret = fixtures::cookie::logged_in(app, credentials, issued_at).await;
+ from_cookie(app, &secret, issued_at).await
}
-pub fn secret(identity: &IdentityToken) -> Secret {
- identity.secret().expect("identity contained a secret")
-}
+pub fn fictitious() -> Identity {
+ let token = token::Id::generate();
+ let login = fixtures::login::fictitious();
-pub fn fictitious() -> IdentityToken {
- let token = Uuid::new_v4().to_string();
- IdentityToken::new().set(token)
+ Identity { token, login }
}
diff --git a/src/test/fixtures/login.rs b/src/test/fixtures/login.rs
index 714b936..e308289 100644
--- a/src/test/fixtures/login.rs
+++ b/src/test/fixtures/login.rs
@@ -8,7 +8,7 @@ use crate::{
name::Name,
};
-pub async fn create_with_password(app: &App, created_at: &RequestedAt) -> (Login, Password) {
+pub async fn create_with_password(app: &App, created_at: &RequestedAt) -> (Name, Password) {
let (name, password) = propose();
let login = app
.logins()
@@ -16,7 +16,7 @@ pub async fn create_with_password(app: &App, created_at: &RequestedAt) -> (Login
.await
.expect("should always succeed if the login is actually new");
- (login, password)
+ (login.name, password)
}
pub async fn create(app: &App, created_at: &RequestedAt) -> Login {
diff --git a/src/test/fixtures/mod.rs b/src/test/fixtures/mod.rs
index 9658831..5609ebc 100644
--- a/src/test/fixtures/mod.rs
+++ b/src/test/fixtures/mod.rs
@@ -3,6 +3,7 @@ use chrono::{TimeDelta, Utc};
use crate::{app::App, clock::RequestedAt, db};
pub mod channel;
+pub mod cookie;
pub mod event;
pub mod future;
pub mod identity;
diff --git a/src/token/extract/identity_token.rs b/src/token/extract/cookie.rs
index a1e900e..af5787d 100644
--- a/src/token/extract/identity_token.rs
+++ b/src/token/extract/cookie.rs
@@ -12,19 +12,21 @@ use crate::token::Secret;
// The usage pattern here - receive the extractor as an argument, return it in
// the response - is heavily modelled after CookieJar's own intended usage.
#[derive(Clone)]
-pub struct IdentityToken {
+pub struct Identity {
cookies: CookieJar,
}
-impl fmt::Debug for IdentityToken {
+impl fmt::Debug for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("IdentityToken")
+ f.debug_struct("IdentityCookie")
.field("identity", &self.secret())
.finish()
}
}
-impl IdentityToken {
+impl Identity {
+ const COOKIE_NAME: &str = "identity";
+
// Creates a new, unpopulated identity token store.
#[cfg(test)]
pub fn new() -> Self {
@@ -40,7 +42,7 @@ impl IdentityToken {
// included.
pub fn secret(&self) -> Option<Secret> {
self.cookies
- .get(IDENTITY_COOKIE)
+ .get(Self::COOKIE_NAME)
.map(Cookie::value)
.map(Secret::from)
}
@@ -49,7 +51,7 @@ impl IdentityToken {
// back to the client when this extractor is included in a response.
pub fn set(self, secret: impl Into<Secret>) -> Self {
let secret = secret.into().reveal();
- let identity_cookie = Cookie::build((IDENTITY_COOKIE, secret))
+ let identity_cookie = Cookie::build((Self::COOKIE_NAME, secret))
.http_only(true)
.path("/")
.permanent()
@@ -64,15 +66,13 @@ impl IdentityToken {
// extractor is included in a response.
pub fn clear(self) -> Self {
Self {
- cookies: self.cookies.remove(IDENTITY_COOKIE),
+ cookies: self.cookies.remove(Self::COOKIE_NAME),
}
}
}
-const IDENTITY_COOKIE: &str = "identity";
-
#[async_trait::async_trait]
-impl<S> FromRequestParts<S> for IdentityToken
+impl<S> FromRequestParts<S> for Identity
where
S: Send + Sync,
{
@@ -84,7 +84,7 @@ where
}
}
-impl IntoResponseParts for IdentityToken {
+impl IntoResponseParts for Identity {
type Error = <CookieJar as IntoResponseParts>::Error;
fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
diff --git a/src/token/extract/identity.rs b/src/token/extract/identity.rs
index 8b3cd94..a69f509 100644
--- a/src/token/extract/identity.rs
+++ b/src/token/extract/identity.rs
@@ -4,7 +4,7 @@ use axum::{
response::{IntoResponse, Response},
};
-use super::IdentityToken;
+use super::IdentityCookie;
use crate::{
app::App,
@@ -25,10 +25,10 @@ impl FromRequestParts<App> for Identity {
type Rejection = LoginError<Internal>;
async fn from_request_parts(parts: &mut Parts, state: &App) -> Result<Self, Self::Rejection> {
- let Ok(identity_token) = IdentityToken::from_request_parts(parts, state).await;
+ let Ok(cookie) = IdentityCookie::from_request_parts(parts, state).await;
let RequestedAt(used_at) = RequestedAt::from_request_parts(parts, state).await?;
- let secret = identity_token.secret().ok_or(LoginError::Unauthorized)?;
+ let secret = cookie.secret().ok_or(LoginError::Unauthorized)?;
let app = State::<App>::from_request_parts(parts, state).await?;
match app.tokens().validate(&secret, &used_at).await {
diff --git a/src/token/extract/mod.rs b/src/token/extract/mod.rs
index b4800ae..fc0f52b 100644
--- a/src/token/extract/mod.rs
+++ b/src/token/extract/mod.rs
@@ -1,4 +1,4 @@
+mod cookie;
mod identity;
-mod identity_token;
-pub use self::{identity::Identity, identity_token::IdentityToken};
+pub use self::{cookie::Identity as IdentityCookie, identity::Identity};
diff --git a/src/ui/assets.rs b/src/ui/assets.rs
index 342ba59..6a7563a 100644
--- a/src/ui/assets.rs
+++ b/src/ui/assets.rs
@@ -1,29 +1,31 @@
+use ::mime::{FromStrError, Mime};
use axum::{
http::{header, StatusCode},
response::{IntoResponse, Response},
};
-use mime_guess::Mime;
use rust_embed::EmbeddedFile;
-use crate::{error::Internal, ui::error::NotFound};
+use super::{error::NotFound, mime};
+use crate::error::Internal;
#[derive(rust_embed::Embed)]
#[folder = "target/ui"]
pub struct Assets;
impl Assets {
- pub fn load(path: impl AsRef<str>) -> Result<Asset, NotFound<String>> {
+ pub fn load(path: impl AsRef<str>) -> Result<Asset, Error> {
let path = path.as_ref();
- let mime = mime_guess::from_path(path).first_or_octet_stream();
+ let mime = mime::from_path(path)?;
Self::get(path)
.map(|file| Asset(mime, file))
- .ok_or(NotFound(format!("not found: {path}")))
+ .ok_or(Error::NotFound(path.into()))
}
pub fn index() -> Result<Asset, Internal> {
// "not found" in this case really is an internal error, as it should
- // never happen. `index.html` is a known-valid path.
+ // never happen. `index.html` is a known-valid path with a known-valid
+ // file extension.
Ok(Self::load("index.html")?)
}
}
@@ -41,3 +43,21 @@ impl IntoResponse for Asset {
.into_response()
}
}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error("not found: {0}")]
+ NotFound(String),
+ #[error(transparent)]
+ Mime(#[from] FromStrError),
+}
+
+impl IntoResponse for Error {
+ fn into_response(self) -> Response {
+ #[allow(clippy::match_wildcard_for_single_variants)]
+ match self {
+ Self::NotFound(_) => NotFound(self.to_string()).into_response(),
+ other => Internal::from(other).into_response(),
+ }
+ }
+}
diff --git a/src/ui/mime.rs b/src/ui/mime.rs
new file mode 100644
index 0000000..9c724f0
--- /dev/null
+++ b/src/ui/mime.rs
@@ -0,0 +1,22 @@
+use mime::Mime;
+use unix_path::Path;
+
+// Extremely manual; using `std::path` here would result in platform-dependent behaviour when it's not appropriate (the URLs passed here always use `/` and are parsed like URLs). Using `unix_path` might be an option, but it's not clearly
+pub fn from_path<P>(path: P) -> Result<Mime, mime::FromStrError>
+where
+ P: AsRef<Path>,
+{
+ let path = path.as_ref();
+ let extension = path.extension().and_then(|ext| ext.to_str());
+ let mime = match extension {
+ Some("css") => "text/css; charset=utf-8",
+ Some("js") => "text/javascript; charset=utf-8",
+ Some("json") => "application/json",
+ Some("html") => "text/html; charset=utf-8",
+ Some("png") => "image/png",
+ _ => "application/octet-stream",
+ };
+ let mime = mime.parse()?;
+
+ Ok(mime)
+}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index c145382..f8caa48 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -1,6 +1,7 @@
mod assets;
mod error;
mod middleware;
+mod mime;
mod routes;
pub use self::routes::router;
diff --git a/src/ui/routes/ch/channel.rs b/src/ui/routes/ch/channel.rs
index 353d000..a338f1f 100644
--- a/src/ui/routes/ch/channel.rs
+++ b/src/ui/routes/ch/channel.rs
@@ -8,7 +8,7 @@ pub mod get {
app::App,
channel,
error::Internal,
- login::Login,
+ token::extract::Identity,
ui::{
assets::{Asset, Assets},
error::NotFound,
@@ -17,10 +17,10 @@ pub mod get {
pub async fn handler(
State(app): State<App>,
- login: Option<Login>,
+ identity: Option<Identity>,
Path(channel): Path<channel::Id>,
) -> Result<Asset, Error> {
- login.ok_or(Error::NotLoggedIn)?;
+ let _ = identity.ok_or(Error::NotLoggedIn)?;
app.channels()
.get(&channel)
.await
diff --git a/src/ui/routes/get.rs b/src/ui/routes/get.rs
index 97737e1..2fcb51c 100644
--- a/src/ui/routes/get.rs
+++ b/src/ui/routes/get.rs
@@ -2,12 +2,12 @@ use axum::response::{self, IntoResponse, Redirect};
use crate::{
error::Internal,
- login::Login,
+ token::extract::Identity,
ui::assets::{Asset, Assets},
};
-pub async fn handler(login: Option<Login>) -> Result<Asset, Error> {
- login.ok_or(Error::NotLoggedIn)?;
+pub async fn handler(identity: Option<Identity>) -> Result<Asset, Error> {
+ let _ = identity.ok_or(Error::NotLoggedIn)?;
Assets::index().map_err(Error::Internal)
}
diff --git a/src/ui/routes/path.rs b/src/ui/routes/path.rs
index 2e9a657..a387552 100644
--- a/src/ui/routes/path.rs
+++ b/src/ui/routes/path.rs
@@ -1,12 +1,9 @@
pub mod get {
use axum::extract::Path;
- use crate::ui::{
- assets::{Asset, Assets},
- error::NotFound,
- };
+ use crate::ui::assets::{Asset, Assets, Error};
- pub async fn handler(Path(path): Path<String>) -> Result<Asset, NotFound<String>> {
+ pub async fn handler(Path(path): Path<String>) -> Result<Asset, Error> {
Assets::load(path)
}
}
diff --git a/tools/run b/tools/run
index 452355e..b063eb7 100755
--- a/tools/run
+++ b/tools/run
@@ -4,5 +4,4 @@
##
## Run the server in development mode. Shorthand for `cargo run`.
-tools/build-ui
cargo run -- "$@"