summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
33 files changed, 278 insertions, 194 deletions
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)
}
}