From ea74daca4809e4008dd8d01039db9fff3be659d9 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Wed, 16 Oct 2024 20:14:33 -0400 Subject: Organizational pass on endpoints and routes. --- src/ui/routes/ch/channel.rs | 61 ++++++++++++++++++++++++++++++++++++++++++ src/ui/routes/ch/mod.rs | 1 + src/ui/routes/get.rs | 30 +++++++++++++++++++++ src/ui/routes/invite/invite.rs | 55 +++++++++++++++++++++++++++++++++++++ src/ui/routes/invite/mod.rs | 4 +++ src/ui/routes/login.rs | 11 ++++++++ src/ui/routes/mod.rs | 26 ++++++++++++++++++ src/ui/routes/path.rs | 12 +++++++++ src/ui/routes/setup.rs | 43 +++++++++++++++++++++++++++++ 9 files changed, 243 insertions(+) create mode 100644 src/ui/routes/ch/channel.rs create mode 100644 src/ui/routes/ch/mod.rs create mode 100644 src/ui/routes/get.rs create mode 100644 src/ui/routes/invite/invite.rs create mode 100644 src/ui/routes/invite/mod.rs create mode 100644 src/ui/routes/login.rs create mode 100644 src/ui/routes/mod.rs create mode 100644 src/ui/routes/path.rs create mode 100644 src/ui/routes/setup.rs (limited to 'src/ui/routes') diff --git a/src/ui/routes/ch/channel.rs b/src/ui/routes/ch/channel.rs new file mode 100644 index 0000000..353d000 --- /dev/null +++ b/src/ui/routes/ch/channel.rs @@ -0,0 +1,61 @@ +pub mod get { + use axum::{ + extract::{Path, State}, + response::{self, IntoResponse, Redirect}, + }; + + use crate::{ + app::App, + channel, + error::Internal, + login::Login, + ui::{ + assets::{Asset, Assets}, + error::NotFound, + }, + }; + + pub async fn handler( + State(app): State, + login: Option, + Path(channel): Path, + ) -> Result { + login.ok_or(Error::NotLoggedIn)?; + app.channels() + .get(&channel) + .await + .map_err(Error::internal)? + .ok_or(Error::NotFound)?; + + Assets::index().map_err(Error::Internal) + } + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error("requested channel not found")] + NotFound, + #[error("not logged in")] + NotLoggedIn, + #[error("{0}")] + Internal(Internal), + } + + impl Error { + fn internal(err: impl Into) -> Self { + Self::Internal(err.into()) + } + } + + impl IntoResponse for Error { + fn into_response(self) -> response::Response { + match self { + Self::NotFound => match Assets::index() { + Ok(asset) => NotFound(asset).into_response(), + Err(internal) => internal.into_response(), + }, + Self::NotLoggedIn => Redirect::temporary("/login").into_response(), + Self::Internal(error) => error.into_response(), + } + } + } +} diff --git a/src/ui/routes/ch/mod.rs b/src/ui/routes/ch/mod.rs new file mode 100644 index 0000000..ff02972 --- /dev/null +++ b/src/ui/routes/ch/mod.rs @@ -0,0 +1 @@ +pub mod channel; diff --git a/src/ui/routes/get.rs b/src/ui/routes/get.rs new file mode 100644 index 0000000..97737e1 --- /dev/null +++ b/src/ui/routes/get.rs @@ -0,0 +1,30 @@ +use axum::response::{self, IntoResponse, Redirect}; + +use crate::{ + error::Internal, + login::Login, + ui::assets::{Asset, Assets}, +}; + +pub async fn handler(login: Option) -> Result { + login.ok_or(Error::NotLoggedIn)?; + + Assets::index().map_err(Error::Internal) +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("not logged in")] + NotLoggedIn, + #[error("{0}")] + Internal(Internal), +} + +impl IntoResponse for Error { + fn into_response(self) -> response::Response { + match self { + Self::NotLoggedIn => Redirect::temporary("/login").into_response(), + Self::Internal(error) => error.into_response(), + } + } +} diff --git a/src/ui/routes/invite/invite.rs b/src/ui/routes/invite/invite.rs new file mode 100644 index 0000000..06e5792 --- /dev/null +++ b/src/ui/routes/invite/invite.rs @@ -0,0 +1,55 @@ +pub mod get { + use axum::{ + extract::{Path, State}, + response::{self, IntoResponse}, + }; + + use crate::{ + app::App, + error::Internal, + invite, + ui::{ + assets::{Asset, Assets}, + error::NotFound, + }, + }; + + pub async fn handler( + State(app): State, + Path(invite): Path, + ) -> Result { + app.invites() + .get(&invite) + .await + .map_err(Error::internal)? + .ok_or(Error::NotFound)?; + + Assets::index().map_err(Error::Internal) + } + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error("invite not found")] + NotFound, + #[error("{0}")] + Internal(Internal), + } + + impl Error { + fn internal(err: impl Into) -> Self { + Self::Internal(err.into()) + } + } + + impl IntoResponse for Error { + fn into_response(self) -> response::Response { + match self { + Self::NotFound => match Assets::index() { + Ok(asset) => NotFound(asset).into_response(), + Err(internal) => internal.into_response(), + }, + Self::Internal(error) => error.into_response(), + } + } + } +} diff --git a/src/ui/routes/invite/mod.rs b/src/ui/routes/invite/mod.rs new file mode 100644 index 0000000..50af8be --- /dev/null +++ b/src/ui/routes/invite/mod.rs @@ -0,0 +1,4 @@ +// In this case, the first redundant `invite` is a literal path segment, and the +// second `invite` reflects a placeholder. +#[allow(clippy::module_inception)] +pub mod invite; diff --git a/src/ui/routes/login.rs b/src/ui/routes/login.rs new file mode 100644 index 0000000..81a874c --- /dev/null +++ b/src/ui/routes/login.rs @@ -0,0 +1,11 @@ +pub mod get { + use crate::{ + error::Internal, + ui::assets::{Asset, Assets}, + }; + + #[allow(clippy::unused_async)] + pub async fn handler() -> Result { + Assets::index() + } +} diff --git a/src/ui/routes/mod.rs b/src/ui/routes/mod.rs new file mode 100644 index 0000000..72d9a4a --- /dev/null +++ b/src/ui/routes/mod.rs @@ -0,0 +1,26 @@ +use axum::{middleware, routing::get, Router}; + +use crate::{app::App, ui::middleware::setup_required}; + +mod ch; +mod get; +mod invite; +mod login; +mod path; +mod setup; + +pub fn router(app: &App) -> Router { + [ + Router::new() + .route("/*path", get(path::get::handler)) + .route("/setup", get(setup::get::handler)), + Router::new() + .route("/", get(get::handler)) + .route("/login", get(login::get::handler)) + .route("/ch/:channel", get(ch::channel::get::handler)) + .route("/invite/:invite", get(invite::invite::get::handler)) + .route_layer(middleware::from_fn_with_state(app.clone(), setup_required)), + ] + .into_iter() + .fold(Router::default(), Router::merge) +} diff --git a/src/ui/routes/path.rs b/src/ui/routes/path.rs new file mode 100644 index 0000000..2e9a657 --- /dev/null +++ b/src/ui/routes/path.rs @@ -0,0 +1,12 @@ +pub mod get { + use axum::extract::Path; + + use crate::ui::{ + assets::{Asset, Assets}, + error::NotFound, + }; + + pub async fn handler(Path(path): Path) -> Result> { + Assets::load(path) + } +} diff --git a/src/ui/routes/setup.rs b/src/ui/routes/setup.rs new file mode 100644 index 0000000..649cc5f --- /dev/null +++ b/src/ui/routes/setup.rs @@ -0,0 +1,43 @@ +pub mod get { + use axum::{ + extract::State, + response::{self, IntoResponse, Redirect}, + }; + + use crate::{ + app::App, + error::Internal, + ui::assets::{Asset, Assets}, + }; + + pub async fn handler(State(app): State) -> Result { + if app + .setup() + .completed() + .await + .map_err(Internal::from) + .map_err(Error::Internal)? + { + Err(Error::SetupCompleted) + } else { + Assets::index().map_err(Error::Internal) + } + } + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error("setup already completed")] + SetupCompleted, + #[error("{0}")] + Internal(Internal), + } + + impl IntoResponse for Error { + fn into_response(self) -> response::Response { + match self { + Self::SetupCompleted => Redirect::to("/login").into_response(), + Self::Internal(error) => error.into_response(), + } + } + } +} -- cgit v1.2.3 From 214a9e6c1fd729fc2c49eb2a5d41b5651ff5bc61 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 22 Oct 2024 22:12:56 -0400 Subject: Set `charset` params on returned content types. This is a somewhat indirect change; it removes `mime_guess` in favour of some very, uh, "bespoke" mime detection logic that hardcodes mime types for the small repertoire of file extensions actually present in the UI. `mime_guess` doesn't provide a way to set params as it exports its own `Mime` struct, which doesn't provide `with_params()`. --- Cargo.lock | 34 +++++++++++++++++----------------- Cargo.toml | 3 ++- src/ui/assets.rs | 32 ++++++++++++++++++++++++++------ src/ui/mime.rs | 22 ++++++++++++++++++++++ src/ui/mod.rs | 1 + src/ui/routes/path.rs | 7 ++----- 6 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 src/ui/mime.rs (limited to 'src/ui/routes') 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", ] @@ -1093,16 +1094,6 @@ version = "0.3.17" 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" @@ -2097,12 +2088,6 @@ version = "1.17.0" 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" @@ -2142,6 +2127,21 @@ version = "0.1.1" 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" 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/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) -> Result> { + pub fn load(path: impl AsRef) -> Result { 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 { // "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

(path: P) -> Result +where + P: AsRef, +{ + 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/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) -> Result> { + pub async fn handler(Path(path): Path) -> Result { Assets::load(path) } } -- cgit v1.2.3 From 01f9f3549c76702fd56e58d44c5180fecddb4bfa Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 22 Oct 2024 23:25:24 -0400 Subject: Sort out the naming of the various parts of an identity. * A `cookie::Identity` (`IdentityCookie`) is a specialized CookieJar for working with identities. * An `Identity` is a token/login pair. I hope for this to be a bit more legible. In service of this, `Login` is no longer extractable. You have to get an identity. --- src/boot/routes/get.rs | 9 ++-- src/boot/routes/test.rs | 6 +-- src/channel/routes/channel/delete.rs | 4 +- src/channel/routes/channel/post.rs | 6 +-- src/channel/routes/channel/test.rs | 52 +++++++++++++++++--- src/channel/routes/post.rs | 4 +- src/channel/routes/test.rs | 6 +-- src/event/routes/test.rs | 26 +++------- src/invite/routes/invite/post.rs | 6 +-- src/invite/routes/post.rs | 10 ++-- src/login/extract.rs | 15 ------ src/login/mod.rs | 1 - src/login/routes/login/post.rs | 6 +-- src/login/routes/login/test.rs | 29 ++++++----- src/login/routes/logout/post.rs | 6 +-- src/login/routes/logout/test.rs | 14 +++--- src/message/app.rs | 2 - src/message/routes/message.rs | 10 ++-- src/setup/routes/post.rs | 6 +-- src/test/fixtures/cookie.rs | 37 ++++++++++++++ src/test/fixtures/identity.rs | 48 ++++++++---------- src/test/fixtures/login.rs | 4 +- src/test/fixtures/mod.rs | 1 + src/token/extract/cookie.rs | 94 ++++++++++++++++++++++++++++++++++++ src/token/extract/identity.rs | 6 +-- src/token/extract/identity_token.rs | 94 ------------------------------------ src/token/extract/mod.rs | 4 +- src/ui/routes/ch/channel.rs | 6 +-- src/ui/routes/get.rs | 6 +-- 29 files changed, 281 insertions(+), 237 deletions(-) delete mode 100644 src/login/extract.rs create mode 100644 src/test/fixtures/cookie.rs create mode 100644 src/token/extract/cookie.rs delete mode 100644 src/token/extract/identity_token.rs (limited to 'src/ui/routes') 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, login: Login) -> Result { +pub async fn handler(State(app): State, identity: Identity) -> Result { 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, Path(channel): Path, RequestedAt(deleted_at): RequestedAt, - _: Login, + _: Identity, ) -> Result { 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, Path(channel): Path, RequestedAt(sent_at): RequestedAt, - login: Login, + identity: Identity, Json(request): Json, ) -> Result { 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, - _: 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, ) -> Result { 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/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, RequestedAt(accepted_at): RequestedAt, - identity: IdentityToken, + identity: IdentityCookie, Path(invite): Path, Json(request): Json, -) -> Result<(IdentityToken, Json), Error> { +) -> Result<(IdentityCookie, Json), 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, RequestedAt(issued_at): RequestedAt, - login: Login, - // Require `{}` as the only valid request for this endpoint. + identity: Identity, _: Json, ) -> Result, 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 for Login { - type Rejection = >::Rejection; - - async fn from_request_parts(parts: &mut Parts, state: &App) -> Result { - 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, RequestedAt(now): RequestedAt, - identity: IdentityToken, + identity: IdentityCookie, Json(request): Json, -) -> Result<(IdentityToken, Json), Error> { +) -> Result<(IdentityCookie, Json), 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, RequestedAt(now): RequestedAt, - identity: IdentityToken, + identity: IdentityCookie, Json(_): Json, -) -> 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 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, Path(message): Path, RequestedAt(deleted_at): RequestedAt, - _: Login, + _: Identity, ) -> Result { 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, RequestedAt(setup_at): RequestedAt, - identity: IdentityToken, + identity: IdentityCookie, Json(request): Json, -) -> Result<(IdentityToken, Json), Error> { +) -> Result<(IdentityCookie, Json), 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/cookie.rs b/src/token/extract/cookie.rs new file mode 100644 index 0000000..af5787d --- /dev/null +++ b/src/token/extract/cookie.rs @@ -0,0 +1,94 @@ +use std::fmt; + +use axum::{ + extract::FromRequestParts, + http::request::Parts, + response::{IntoResponseParts, ResponseParts}, +}; +use axum_extra::extract::cookie::{Cookie, CookieJar}; + +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 Identity { + cookies: CookieJar, +} + +impl fmt::Debug for Identity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IdentityCookie") + .field("identity", &self.secret()) + .finish() + } +} + +impl Identity { + const COOKIE_NAME: &str = "identity"; + + // Creates a new, unpopulated identity token store. + #[cfg(test)] + pub fn new() -> Self { + Self { + cookies: CookieJar::new(), + } + } + + // Get the identity secret sent in the request, if any. If the identity + // was not sent, or if it has previously been [clear]ed, then this will + // return [None]. If the identity has previously been [set], then this + // will return that secret, regardless of what the request originally + // included. + pub fn secret(&self) -> Option { + self.cookies + .get(Self::COOKIE_NAME) + .map(Cookie::value) + .map(Secret::from) + } + + // Positively set the identity secret, and ensure that it will be sent + // back to the client when this extractor is included in a response. + pub fn set(self, secret: impl Into) -> Self { + let secret = secret.into().reveal(); + let identity_cookie = Cookie::build((Self::COOKIE_NAME, secret)) + .http_only(true) + .path("/") + .permanent() + .build(); + + Self { + cookies: self.cookies.add(identity_cookie), + } + } + + // Remove the identity secret and ensure that it will be cleared when this + // extractor is included in a response. + pub fn clear(self) -> Self { + Self { + cookies: self.cookies.remove(Self::COOKIE_NAME), + } + } +} + +#[async_trait::async_trait] +impl FromRequestParts for Identity +where + S: Send + Sync, +{ + type Rejection = >::Rejection; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let cookies = CookieJar::from_request_parts(parts, state).await?; + Ok(Self { cookies }) + } +} + +impl IntoResponseParts for Identity { + type Error = ::Error; + + fn into_response_parts(self, res: ResponseParts) -> Result { + let Self { cookies } = self; + cookies.into_response_parts(res) + } +} 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 for Identity { type Rejection = LoginError; async fn from_request_parts(parts: &mut Parts, state: &App) -> Result { - 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::::from_request_parts(parts, state).await?; match app.tokens().validate(&secret, &used_at).await { diff --git a/src/token/extract/identity_token.rs b/src/token/extract/identity_token.rs deleted file mode 100644 index a1e900e..0000000 --- a/src/token/extract/identity_token.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::fmt; - -use axum::{ - extract::FromRequestParts, - http::request::Parts, - response::{IntoResponseParts, ResponseParts}, -}; -use axum_extra::extract::cookie::{Cookie, CookieJar}; - -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 { - cookies: CookieJar, -} - -impl fmt::Debug for IdentityToken { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("IdentityToken") - .field("identity", &self.secret()) - .finish() - } -} - -impl IdentityToken { - // Creates a new, unpopulated identity token store. - #[cfg(test)] - pub fn new() -> Self { - Self { - cookies: CookieJar::new(), - } - } - - // Get the identity secret sent in the request, if any. If the identity - // was not sent, or if it has previously been [clear]ed, then this will - // return [None]. If the identity has previously been [set], then this - // will return that secret, regardless of what the request originally - // included. - pub fn secret(&self) -> Option { - self.cookies - .get(IDENTITY_COOKIE) - .map(Cookie::value) - .map(Secret::from) - } - - // Positively set the identity secret, and ensure that it will be sent - // back to the client when this extractor is included in a response. - pub fn set(self, secret: impl Into) -> Self { - let secret = secret.into().reveal(); - let identity_cookie = Cookie::build((IDENTITY_COOKIE, secret)) - .http_only(true) - .path("/") - .permanent() - .build(); - - Self { - cookies: self.cookies.add(identity_cookie), - } - } - - // Remove the identity secret and ensure that it will be cleared when this - // extractor is included in a response. - pub fn clear(self) -> Self { - Self { - cookies: self.cookies.remove(IDENTITY_COOKIE), - } - } -} - -const IDENTITY_COOKIE: &str = "identity"; - -#[async_trait::async_trait] -impl FromRequestParts for IdentityToken -where - S: Send + Sync, -{ - type Rejection = >::Rejection; - - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let cookies = CookieJar::from_request_parts(parts, state).await?; - Ok(Self { cookies }) - } -} - -impl IntoResponseParts for IdentityToken { - type Error = ::Error; - - fn into_response_parts(self, res: ResponseParts) -> Result { - let Self { cookies } = self; - cookies.into_response_parts(res) - } -} 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/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, - login: Option, + identity: Option, Path(channel): Path, ) -> Result { - 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) -> Result { - login.ok_or(Error::NotLoggedIn)?; +pub async fn handler(identity: Option) -> Result { + let _ = identity.ok_or(Error::NotLoggedIn)?; Assets::index().map_err(Error::Internal) } -- cgit v1.2.3