summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorojacobson <ojacobson@noreply.codeberg.org>2025-06-21 04:22:52 +0200
committerojacobson <ojacobson@noreply.codeberg.org>2025-06-21 04:22:52 +0200
commitcd1dc0dab4b46bc5712070812192d5ce34071470 (patch)
treec94f5a42f7e734b81892c1289a1d2b566706ba7c /src
parentd84ba5cd09b713fac2f193d5c05af7415ea6742d (diff)
parent4e3d5ccac99b24934c972e088cd7eb02bb95df06 (diff)
Reorganize and consolidate HTTP routes.
HTTP routes are now defined in a single, unified module, pulling them out of the topical modules they were formerly part of. This is intended to improve the navigability of the codebase. Previously, finding the handler corresponding to a specific endpoint required prior familiarity, though in practice you could usually guess from topic area. Now, all routes are defined in one place; if you know the path, you can read down the list to find the handler. Handlers themselves live with the domain they are most appropriately "part of," generally (in this version, universally) in a `handlers` submodule. The handlers themselves have been flattened down; rather than representing a path and a method, they now represent a named operation (which is suspiciously similar to the path in most cases). This means that we no longer have constructs like `crate::ui::routes::ch::channel` - it's now `crate::ui::handlers::channel` instead. ## Disclaimer I Solemnly Swear I Didn't Change Any Handlers. ## Prior art I've inadvertently reinvented Django's `urls.py` convention, and I've opted to lean into that. Merges flatter-routes-reorg into main.
Diffstat (limited to 'src')
-rw-r--r--src/boot/handlers/boot/mod.rs (renamed from src/boot/routes/get.rs)3
-rw-r--r--src/boot/handlers/boot/test.rs (renamed from src/boot/routes/test.rs)17
-rw-r--r--src/boot/handlers/mod.rs3
-rw-r--r--src/boot/mod.rs10
-rw-r--r--src/boot/routes/mod.rs11
-rw-r--r--src/broadcast.rs7
-rw-r--r--src/channel/handlers/create/mod.rs (renamed from src/channel/routes/post.rs)3
-rw-r--r--src/channel/handlers/create/test.rs (renamed from src/channel/routes/test.rs)53
-rw-r--r--src/channel/handlers/delete/mod.rs (renamed from src/channel/routes/channel/delete.rs)7
-rw-r--r--src/channel/handlers/delete/test.rs (renamed from src/channel/routes/channel/test/delete.rs)17
-rw-r--r--src/channel/handlers/mod.rs9
-rw-r--r--src/channel/handlers/send/mod.rs (renamed from src/channel/routes/channel/post.rs)6
-rw-r--r--src/channel/handlers/send/test.rs (renamed from src/channel/routes/channel/test/post.rs)14
-rw-r--r--src/channel/mod.rs4
-rw-r--r--src/channel/routes/channel/mod.rs9
-rw-r--r--src/channel/routes/channel/test/mod.rs2
-rw-r--r--src/channel/routes/mod.rs19
-rw-r--r--src/cli.rs39
-rw-r--r--src/event/handlers/mod.rs3
-rw-r--r--src/event/handlers/stream/mod.rs (renamed from src/event/routes/get.rs)3
-rw-r--r--src/event/handlers/stream/test/channel.rs (renamed from src/event/routes/test/channel.rs)33
-rw-r--r--src/event/handlers/stream/test/invite.rs (renamed from src/event/routes/test/invite.rs)13
-rw-r--r--src/event/handlers/stream/test/message.rs (renamed from src/event/routes/test/message.rs)41
-rw-r--r--src/event/handlers/stream/test/mod.rs (renamed from src/event/routes/test/mod.rs)2
-rw-r--r--src/event/handlers/stream/test/resume.rs (renamed from src/event/routes/test/resume.rs)22
-rw-r--r--src/event/handlers/stream/test/setup.rs (renamed from src/event/routes/test/setup.rs)9
-rw-r--r--src/event/handlers/stream/test/token.rs (renamed from src/event/routes/test/token.rs)17
-rw-r--r--src/event/mod.rs10
-rw-r--r--src/event/routes/mod.rs11
-rw-r--r--src/invite/handlers/accept/mod.rs (renamed from src/invite/routes/invite/post.rs)7
-rw-r--r--src/invite/handlers/accept/test.rs (renamed from src/invite/routes/invite/test/post.rs)30
-rw-r--r--src/invite/handlers/get/mod.rs (renamed from src/invite/routes/invite/get.rs)7
-rw-r--r--src/invite/handlers/get/test.rs (renamed from src/invite/routes/invite/test/get.rs)12
-rw-r--r--src/invite/handlers/issue/mod.rs (renamed from src/invite/routes/post.rs)3
-rw-r--r--src/invite/handlers/issue/test.rs (renamed from src/invite/routes/test.rs)5
-rw-r--r--src/invite/handlers/mod.rs9
-rw-r--r--src/invite/mod.rs8
-rw-r--r--src/invite/routes/invite/mod.rs6
-rw-r--r--src/invite/routes/invite/test/mod.rs2
-rw-r--r--src/invite/routes/mod.rs18
-rw-r--r--src/lib.rs1
-rw-r--r--src/message/handlers/delete/mod.rs55
-rw-r--r--src/message/handlers/delete/test.rs (renamed from src/message/routes/message/test.rs)13
-rw-r--r--src/message/handlers/mod.rs3
-rw-r--r--src/message/mod.rs6
-rw-r--r--src/message/routes/message/mod.rs61
-rw-r--r--src/message/routes/mod.rs9
-rw-r--r--src/routes.rs62
-rw-r--r--src/setup/handlers/mod.rs3
-rw-r--r--src/setup/handlers/setup/mod.rs (renamed from src/setup/routes/post.rs)3
-rw-r--r--src/setup/handlers/setup/test.rs (renamed from src/setup/routes/test.rs)17
-rw-r--r--src/setup/mod.rs4
-rw-r--r--src/setup/routes/mod.rs11
-rw-r--r--src/ui/handlers/asset.rs7
-rw-r--r--src/ui/handlers/channel.rs58
-rw-r--r--src/ui/handlers/index.rs (renamed from src/ui/routes/get.rs)0
-rw-r--r--src/ui/handlers/invite.rs53
-rw-r--r--src/ui/handlers/login.rs8
-rw-r--r--src/ui/handlers/me.rs30
-rw-r--r--src/ui/handlers/mod.rs15
-rw-r--r--src/ui/handlers/setup.rs41
-rw-r--r--src/ui/mod.rs4
-rw-r--r--src/ui/routes/ch/channel.rs60
-rw-r--r--src/ui/routes/ch/mod.rs1
-rw-r--r--src/ui/routes/invite/invite.rs55
-rw-r--r--src/ui/routes/invite/mod.rs4
-rw-r--r--src/ui/routes/login.rs10
-rw-r--r--src/ui/routes/me.rs32
-rw-r--r--src/ui/routes/mod.rs28
-rw-r--r--src/ui/routes/path.rs9
-rw-r--r--src/ui/routes/setup.rs43
-rw-r--r--src/user/handlers/login/mod.rs (renamed from src/user/routes/login/post.rs)3
-rw-r--r--src/user/handlers/login/test.rs (renamed from src/user/routes/login/test.rs)21
-rw-r--r--src/user/handlers/logout/mod.rs (renamed from src/user/routes/logout/post.rs)3
-rw-r--r--src/user/handlers/logout/test.rs (renamed from src/user/routes/logout/test.rs)12
-rw-r--r--src/user/handlers/mod.rs7
-rw-r--r--src/user/handlers/password/mod.rs (renamed from src/user/routes/password/post.rs)3
-rw-r--r--src/user/handlers/password/test.rs (renamed from src/user/routes/password/test.rs)5
-rw-r--r--src/user/mod.rs6
-rw-r--r--src/user/routes/login/mod.rs4
-rw-r--r--src/user/routes/logout/mod.rs4
-rw-r--r--src/user/routes/mod.rs14
-rw-r--r--src/user/routes/password/mod.rs4
83 files changed, 604 insertions, 692 deletions
diff --git a/src/boot/routes/get.rs b/src/boot/handlers/boot/mod.rs
index 4873b7a..010f57b 100644
--- a/src/boot/routes/get.rs
+++ b/src/boot/handlers/boot/mod.rs
@@ -5,6 +5,9 @@ use axum::{
use crate::{app::App, boot::Snapshot, error::Internal, token::extract::Identity, user::User};
+#[cfg(test)]
+mod test;
+
pub async fn handler(State(app): State<App>, identity: Identity) -> Result<Response, Internal> {
let snapshot = app.boot().snapshot().await?;
Ok(Response {
diff --git a/src/boot/routes/test.rs b/src/boot/handlers/boot/test.rs
index 55802fe..0a7622b 100644
--- a/src/boot/routes/test.rs
+++ b/src/boot/handlers/boot/test.rs
@@ -1,6 +1,5 @@
use axum::extract::State;
-use super::get;
use crate::test::fixtures;
#[tokio::test]
@@ -8,7 +7,7 @@ async fn returns_identity() {
let app = fixtures::scratch_app().await;
let viewer = fixtures::identity::fictitious();
- let response = get::handler(State(app), viewer.clone())
+ let response = super::handler(State(app), viewer.clone())
.await
.expect("boot always succeeds");
@@ -21,7 +20,7 @@ async fn includes_logins() {
let spectator = fixtures::user::create(&app, &fixtures::now()).await;
let viewer = fixtures::identity::fictitious();
- let response = get::handler(State(app), viewer)
+ let response = super::handler(State(app), viewer)
.await
.expect("boot always succeeds");
@@ -34,7 +33,7 @@ async fn includes_channels() {
let channel = fixtures::channel::create(&app, &fixtures::now()).await;
let viewer = fixtures::identity::fictitious();
- let response = get::handler(State(app), viewer)
+ let response = super::handler(State(app), viewer)
.await
.expect("boot always succeeds");
@@ -49,7 +48,7 @@ async fn includes_messages() {
let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await;
let viewer = fixtures::identity::fictitious();
- let response = get::handler(State(app), viewer)
+ let response = super::handler(State(app), viewer)
.await
.expect("boot always succeeds");
@@ -70,7 +69,7 @@ async fn excludes_expired_messages() {
.expect("expiry never fails");
let viewer = fixtures::identity::fictitious();
- let response = get::handler(State(app), viewer)
+ let response = super::handler(State(app), viewer)
.await
.expect("boot always succeeds");
@@ -90,7 +89,7 @@ async fn excludes_deleted_messages() {
.expect("deleting valid message succeeds");
let viewer = fixtures::identity::fictitious();
- let response = get::handler(State(app), viewer)
+ let response = super::handler(State(app), viewer)
.await
.expect("boot always succeeds");
@@ -108,7 +107,7 @@ async fn excludes_expired_channels() {
.expect("expiry never fails");
let viewer = fixtures::identity::fictitious();
- let response = get::handler(State(app), viewer)
+ let response = super::handler(State(app), viewer)
.await
.expect("boot always succeeds");
@@ -126,7 +125,7 @@ async fn excludes_deleted_channels() {
.expect("deleting a valid channel succeeds");
let viewer = fixtures::identity::fictitious();
- let response = get::handler(State(app), viewer)
+ let response = super::handler(State(app), viewer)
.await
.expect("boot always succeeds");
diff --git a/src/boot/handlers/mod.rs b/src/boot/handlers/mod.rs
new file mode 100644
index 0000000..194c4a9
--- /dev/null
+++ b/src/boot/handlers/mod.rs
@@ -0,0 +1,3 @@
+mod boot;
+
+pub use boot::handler as boot;
diff --git a/src/boot/mod.rs b/src/boot/mod.rs
index 122bd53..48da4f0 100644
--- a/src/boot/mod.rs
+++ b/src/boot/mod.rs
@@ -1,11 +1,11 @@
-use crate::{channel::Channel, event::Sequence, message::Message, user::User};
-use serde::Serialize;
use std::time::Duration;
-pub mod app;
-mod routes;
+use serde::Serialize;
-pub use self::routes::router;
+use crate::{channel::Channel, event::Sequence, message::Message, user::User};
+
+pub mod app;
+pub mod handlers;
#[derive(serde::Serialize)]
pub struct Snapshot {
diff --git a/src/boot/routes/mod.rs b/src/boot/routes/mod.rs
deleted file mode 100644
index 8fd99d3..0000000
--- a/src/boot/routes/mod.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use axum::{Router, routing::get};
-
-use crate::app::App;
-
-mod get;
-#[cfg(test)]
-mod test;
-
-pub fn router() -> Router<App> {
- Router::new().route("/api/boot", get(get::handler))
-}
diff --git a/src/broadcast.rs b/src/broadcast.rs
index 174016e..41a5e23 100644
--- a/src/broadcast.rs
+++ b/src/broadcast.rs
@@ -58,10 +58,9 @@ where
#[allow(clippy::manual_ok_err)]
future::ready(match r {
Ok(event) => Some(event),
- // Stop the stream here. This will disconnect SSE clients
- // (see `routes.rs`), who will then resume from
- // `Last-Event-ID`, allowing them to catch up by reading
- // the skipped messages from the database.
+ // Stop the stream here. This will disconnect SSE clients (see the `/api/events`
+ // endpoint), who will then resume from `Last-Event-ID`, allowing them to catch up
+ // by reading the skipped messages from the database.
//
// See also:
// <https://users.rust-lang.org/t/taking-from-stream-while-ok/48854>
diff --git a/src/channel/routes/post.rs b/src/channel/handlers/create/mod.rs
index 6ea9b61..2c860fc 100644
--- a/src/channel/routes/post.rs
+++ b/src/channel/handlers/create/mod.rs
@@ -13,6 +13,9 @@ use crate::{
token::extract::Identity,
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
_: Identity, // requires auth, but doesn't actually care who you are
diff --git a/src/channel/routes/test.rs b/src/channel/handlers/create/test.rs
index cba8f2e..3c770cf 100644
--- a/src/channel/routes/test.rs
+++ b/src/channel/handlers/create/test.rs
@@ -3,7 +3,6 @@ use std::future;
use axum::extract::{Json, State};
use futures::stream::StreamExt as _;
-use super::post;
use crate::{
channel::app,
name::Name,
@@ -21,9 +20,9 @@ async fn new_channel() {
// Call the endpoint
let name = fixtures::channel::propose();
- let request = post::Request { name: name.clone() };
- let post::Response(response) =
- post::handler(State(app.clone()), creator, fixtures::now(), Json(request))
+ let request = super::Request { name: name.clone() };
+ let super::Response(response) =
+ super::handler(State(app.clone()), creator, fixtures::now(), Json(request))
.await
.expect("creating a channel in an empty app succeeds");
@@ -67,11 +66,11 @@ async fn duplicate_name() {
// Call the endpoint
- let request = post::Request {
+ let request = super::Request {
name: channel.name.clone(),
};
- let post::Error(error) =
- post::handler(State(app.clone()), creator, fixtures::now(), Json(request))
+ let super::Error(error) =
+ super::handler(State(app.clone()), creator, fixtures::now(), Json(request))
.await
.expect_err("duplicate channel name should fail the request");
@@ -100,11 +99,11 @@ async fn conflicting_canonical_name() {
// Call the endpoint
- let request = post::Request {
+ let request = super::Request {
name: conflicting_name.clone(),
};
- let post::Error(error) =
- post::handler(State(app.clone()), creator, fixtures::now(), Json(request))
+ let super::Error(error) =
+ super::handler(State(app.clone()), creator, fixtures::now(), Json(request))
.await
.expect_err("duplicate channel name should fail the request");
@@ -126,11 +125,15 @@ async fn invalid_name() {
// Call the endpoint
let name = fixtures::channel::propose_invalid_name();
- let request = post::Request { name: name.clone() };
- let post::Error(error) =
- post::handler(State(app.clone()), creator, fixtures::now(), Json(request))
- .await
- .expect_err("invalid channel name should fail the request");
+ let request = super::Request { name: name.clone() };
+ let super::Error(error) = crate::channel::handlers::create::handler(
+ State(app.clone()),
+ creator,
+ fixtures::now(),
+ Json(request),
+ )
+ .await
+ .expect_err("invalid channel name should fail the request");
// Verify the structure of the response
@@ -150,8 +153,8 @@ async fn name_reusable_after_delete() {
// Call the endpoint (first time)
- let request = post::Request { name: name.clone() };
- let post::Response(response) = post::handler(
+ let request = super::Request { name: name.clone() };
+ let super::Response(response) = super::handler(
State(app.clone()),
creator.clone(),
fixtures::now(),
@@ -169,9 +172,9 @@ async fn name_reusable_after_delete() {
// Call the endpoint (second time)
- let request = post::Request { name: name.clone() };
- let post::Response(response) =
- post::handler(State(app.clone()), creator, fixtures::now(), Json(request))
+ let request = super::Request { name: name.clone() };
+ let super::Response(response) =
+ super::handler(State(app.clone()), creator, fixtures::now(), Json(request))
.await
.expect("creation succeeds after original channel deleted");
@@ -199,8 +202,8 @@ async fn name_reusable_after_expiry() {
// Call the endpoint (first time)
- let request = post::Request { name: name.clone() };
- let post::Response(_) = post::handler(
+ let request = super::Request { name: name.clone() };
+ let super::Response(_) = super::handler(
State(app.clone()),
creator.clone(),
fixtures::ancient(),
@@ -218,9 +221,9 @@ async fn name_reusable_after_expiry() {
// Call the endpoint (second time)
- let request = post::Request { name: name.clone() };
- let post::Response(response) =
- post::handler(State(app.clone()), creator, fixtures::now(), Json(request))
+ let request = super::Request { name: name.clone() };
+ let super::Response(response) =
+ super::handler(State(app.clone()), creator, fixtures::now(), Json(request))
.await
.expect("creation succeeds after original channel expired");
diff --git a/src/channel/routes/channel/delete.rs b/src/channel/handlers/delete/mod.rs
index 3db7772..b986bec 100644
--- a/src/channel/routes/channel/delete.rs
+++ b/src/channel/handlers/delete/mod.rs
@@ -6,15 +6,18 @@ use axum::{
use crate::{
app::App,
- channel::{self, app},
+ channel::{self, app, handlers::PathInfo},
clock::RequestedAt,
error::{Internal, NotFound},
token::extract::Identity,
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
- Path(channel): Path<super::PathInfo>,
+ Path(channel): Path<PathInfo>,
RequestedAt(deleted_at): RequestedAt,
_: Identity,
) -> Result<Response, Error> {
diff --git a/src/channel/routes/channel/test/delete.rs b/src/channel/handlers/delete/test.rs
index bd9261d..b1e42ea 100644
--- a/src/channel/routes/channel/test/delete.rs
+++ b/src/channel/handlers/delete/test.rs
@@ -1,9 +1,6 @@
use axum::extract::{Path, State};
-use crate::{
- channel::{app, routes::channel::delete},
- test::fixtures,
-};
+use crate::{channel::app, test::fixtures};
#[tokio::test]
pub async fn valid_channel() {
@@ -15,7 +12,7 @@ pub async fn valid_channel() {
// Send the request
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
- let response = delete::handler(
+ let response = super::handler(
State(app.clone()),
Path(channel.id.clone()),
fixtures::now(),
@@ -44,7 +41,7 @@ pub async fn invalid_channel_id() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let channel = fixtures::channel::fictitious();
- let delete::Error(error) = delete::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
Path(channel.clone()),
fixtures::now(),
@@ -73,7 +70,7 @@ pub async fn channel_deleted() {
// Send the request
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
- let delete::Error(error) = delete::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
Path(channel.id.clone()),
fixtures::now(),
@@ -102,7 +99,7 @@ pub async fn channel_expired() {
// Send the request
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
- let delete::Error(error) = delete::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
Path(channel.id.clone()),
fixtures::now(),
@@ -136,7 +133,7 @@ pub async fn channel_purged() {
// Send the request
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
- let delete::Error(error) = delete::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
Path(channel.id.clone()),
fixtures::now(),
@@ -162,7 +159,7 @@ pub async fn channel_not_empty() {
// Send the request
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
- let delete::Error(error) = delete::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
Path(channel.id.clone()),
fixtures::now(),
diff --git a/src/channel/handlers/mod.rs b/src/channel/handlers/mod.rs
new file mode 100644
index 0000000..f2ffd0d
--- /dev/null
+++ b/src/channel/handlers/mod.rs
@@ -0,0 +1,9 @@
+mod create;
+mod delete;
+mod send;
+
+pub use create::handler as create;
+pub use delete::handler as delete;
+pub use send::handler as send;
+
+type PathInfo = crate::channel::Id;
diff --git a/src/channel/routes/channel/post.rs b/src/channel/handlers/send/mod.rs
index 2547122..aa241e2 100644
--- a/src/channel/routes/channel/post.rs
+++ b/src/channel/handlers/send/mod.rs
@@ -4,6 +4,7 @@ use axum::{
response::{self, IntoResponse},
};
+use crate::channel::handlers::PathInfo;
use crate::{
app::App,
clock::RequestedAt,
@@ -12,9 +13,12 @@ use crate::{
token::extract::Identity,
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
- Path(channel): Path<super::PathInfo>,
+ Path(channel): Path<PathInfo>,
RequestedAt(sent_at): RequestedAt,
identity: Identity,
Json(request): Json<Request>,
diff --git a/src/channel/routes/channel/test/post.rs b/src/channel/handlers/send/test.rs
index d9527ac..f43f901 100644
--- a/src/channel/routes/channel/test/post.rs
+++ b/src/channel/handlers/send/test.rs
@@ -2,7 +2,7 @@ use axum::extract::{Json, Path, State};
use futures::stream::{self, StreamExt as _};
use crate::{
- channel::{self, routes::channel::post},
+ channel,
event::Sequenced,
message::app::SendError,
test::fixtures::{self, future::Expect as _},
@@ -25,9 +25,9 @@ async fn messages_in_order() {
];
for (sent_at, body) in &requests {
- let request = post::Request { body: body.clone() };
+ let request = super::Request { body: body.clone() };
- let _ = post::handler(
+ let _ = super::handler(
State(app.clone()),
Path(channel.id.clone()),
sent_at.clone(),
@@ -71,10 +71,10 @@ async fn nonexistent_channel() {
let sent_at = fixtures::now();
let channel = channel::Id::generate();
- let request = post::Request {
+ let request = super::Request {
body: fixtures::message::propose(),
};
- let post::Error(error) = post::handler(
+ let super::Error(error) = super::handler(
State(app),
Path(channel.clone()),
sent_at,
@@ -109,10 +109,10 @@ async fn deleted_channel() {
let sent_at = fixtures::now();
let channel = channel::Id::generate();
- let request = post::Request {
+ let request = super::Request {
body: fixtures::message::propose(),
};
- let post::Error(error) = post::handler(
+ let super::Error(error) = super::handler(
State(app),
Path(channel.clone()),
sent_at,
diff --git a/src/channel/mod.rs b/src/channel/mod.rs
index d5ba828..bbaf33e 100644
--- a/src/channel/mod.rs
+++ b/src/channel/mod.rs
@@ -1,10 +1,10 @@
pub mod app;
pub mod event;
+pub mod handlers;
mod history;
mod id;
pub mod repo;
-mod routes;
mod snapshot;
mod validate;
-pub use self::{event::Event, history::History, id::Id, routes::router, snapshot::Channel};
+pub use self::{event::Event, history::History, id::Id, snapshot::Channel};
diff --git a/src/channel/routes/channel/mod.rs b/src/channel/routes/channel/mod.rs
deleted file mode 100644
index 31a9142..0000000
--- a/src/channel/routes/channel/mod.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use crate::channel::Id;
-
-pub mod delete;
-pub mod post;
-
-#[cfg(test)]
-mod test;
-
-type PathInfo = Id;
diff --git a/src/channel/routes/channel/test/mod.rs b/src/channel/routes/channel/test/mod.rs
deleted file mode 100644
index 78bf86e..0000000
--- a/src/channel/routes/channel/test/mod.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-mod delete;
-mod post;
diff --git a/src/channel/routes/mod.rs b/src/channel/routes/mod.rs
deleted file mode 100644
index c917348..0000000
--- a/src/channel/routes/mod.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-use axum::{
- Router,
- routing::{delete, post},
-};
-
-use crate::app::App;
-
-mod channel;
-mod post;
-
-#[cfg(test)]
-mod test;
-
-pub fn router() -> Router<App> {
- Router::new()
- .route("/api/channels", post(post::handler))
- .route("/api/channels/{channel}", post(channel::post::handler))
- .route("/api/channels/{channel}", delete(channel::delete::handler))
-}
diff --git a/src/cli.rs b/src/cli.rs
index 7bfdbc0..28c2ec8 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -6,7 +6,6 @@
use std::{future, io};
use axum::{
- Router,
http::header,
middleware,
response::{IntoResponse, Response},
@@ -15,7 +14,7 @@ use clap::{CommandFactory, Parser};
use sqlx::sqlite::SqlitePool;
use tokio::net;
-use crate::{app::App, boot, channel, clock, db, event, expire, invite, message, setup, ui, user};
+use crate::{app::App, clock, db, routes};
/// Command-line entry point for running the `pilcrow` server.
///
@@ -82,7 +81,7 @@ impl Args {
let pool = self.pool().await?;
let app = App::from(pool);
- let app = routers(&app)
+ let app = routes::routes(&app)
.route_layer(middleware::from_fn(clock::middleware))
.route_layer(middleware::map_response(Self::server_info()))
.with_state(app);
@@ -123,40 +122,6 @@ impl Args {
}
}
-fn routers(app: &App) -> Router<App> {
- [
- [
- // API endpoints that require setup to function
- boot::router(),
- channel::router(),
- event::router(),
- invite::router(),
- user::router(),
- message::router(),
- ]
- .into_iter()
- .fold(Router::default(), Router::merge)
- // Run expiry whenever someone accesses the API. This was previously a blanket middleware
- // affecting the whole service, but loading the client makes a several requests before the
- // client can completely load, each of which was triggering expiry. There is absolutely no
- // upside to re-checking expiry tens of times back-to-back like that; the API is accessed
- // more regularly and with less of a traffic rush.
- //
- // This should, probably, be moved to a background job at some point.
- .route_layer(middleware::from_fn_with_state(
- app.clone(),
- expire::middleware,
- ))
- .route_layer(setup::Required(app.clone())),
- // API endpoints that handle setup
- setup::router(),
- // The UI (handles setup state itself)
- ui::router(app),
- ]
- .into_iter()
- .fold(Router::default(), Router::merge)
-}
-
fn started_msg(listener: &net::TcpListener) -> io::Result<String> {
let local_addr = listener.local_addr()?;
Ok(format!("listening on http://{local_addr}/"))
diff --git a/src/event/handlers/mod.rs b/src/event/handlers/mod.rs
new file mode 100644
index 0000000..22d988c
--- /dev/null
+++ b/src/event/handlers/mod.rs
@@ -0,0 +1,3 @@
+mod stream;
+
+pub use stream::handler as stream;
diff --git a/src/event/routes/get.rs b/src/event/handlers/stream/mod.rs
index f6c91fa..d0d3f08 100644
--- a/src/event/routes/get.rs
+++ b/src/event/handlers/stream/mod.rs
@@ -15,6 +15,9 @@ use crate::{
token::{app::ValidateError, extract::Identity},
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
identity: Identity,
diff --git a/src/event/routes/test/channel.rs b/src/event/handlers/stream/test/channel.rs
index 0695ab1..187c3c3 100644
--- a/src/event/routes/test/channel.rs
+++ b/src/event/handlers/stream/test/channel.rs
@@ -2,10 +2,7 @@ use axum::extract::State;
use axum_extra::extract::Query;
use futures::{future, stream::StreamExt as _};
-use crate::{
- event::routes::get,
- test::fixtures::{self, future::Expect as _},
-};
+use crate::test::fixtures::{self, future::Expect as _};
#[tokio::test]
async fn creating() {
@@ -17,11 +14,11 @@ async fn creating() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -65,11 +62,11 @@ async fn previously_created() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -96,11 +93,11 @@ async fn expiring() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -140,11 +137,11 @@ async fn previously_expired() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -170,11 +167,11 @@ async fn deleting() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -214,11 +211,11 @@ async fn previously_deleted() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -256,11 +253,11 @@ async fn previously_purged() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
diff --git a/src/event/routes/test/invite.rs b/src/event/handlers/stream/test/invite.rs
index 1d1bec6..c8e12fb 100644
--- a/src/event/routes/test/invite.rs
+++ b/src/event/handlers/stream/test/invite.rs
@@ -2,10 +2,7 @@ use axum::extract::State;
use axum_extra::extract::Query;
use futures::{future, stream::StreamExt as _};
-use crate::{
- event::routes::get,
- test::fixtures::{self, future::Expect as _},
-};
+use crate::test::fixtures::{self, future::Expect as _};
#[tokio::test]
async fn accepting_invite() {
@@ -19,11 +16,11 @@ async fn accepting_invite() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -69,11 +66,11 @@ async fn previously_accepted_invite() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
diff --git a/src/event/routes/test/message.rs b/src/event/handlers/stream/test/message.rs
index 84a3aec..a80c896 100644
--- a/src/event/routes/test/message.rs
+++ b/src/event/handlers/stream/test/message.rs
@@ -5,10 +5,7 @@ use futures::{
stream::{self, StreamExt as _},
};
-use crate::{
- event::routes::get,
- test::fixtures::{self, future::Expect as _},
-};
+use crate::test::fixtures::{self, future::Expect as _};
#[tokio::test]
async fn sending() {
@@ -21,11 +18,11 @@ async fn sending() {
// Call the endpoint
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -80,11 +77,11 @@ async fn previously_sent() {
// Call the endpoint
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -126,11 +123,11 @@ async fn sent_in_multiple_channels() {
// Call the endpoint
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -168,11 +165,11 @@ async fn sent_sequentially() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -207,11 +204,11 @@ async fn expiring() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -253,11 +250,11 @@ async fn previously_expired() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -285,11 +282,11 @@ async fn deleting() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -331,11 +328,11 @@ async fn previously_deleted() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -375,11 +372,11 @@ async fn previously_purged() {
// Subscribe
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
diff --git a/src/event/routes/test/mod.rs b/src/event/handlers/stream/test/mod.rs
index e7e35f1..df43deb 100644
--- a/src/event/routes/test/mod.rs
+++ b/src/event/handlers/stream/test/mod.rs
@@ -4,3 +4,5 @@ mod message;
mod resume;
mod setup;
mod token;
+
+use super::{QueryParams, Response, handler};
diff --git a/src/event/routes/test/resume.rs b/src/event/handlers/stream/test/resume.rs
index 633eae3..34fee4d 100644
--- a/src/event/routes/test/resume.rs
+++ b/src/event/handlers/stream/test/resume.rs
@@ -5,7 +5,7 @@ use axum_extra::extract::Query;
use futures::stream::{self, StreamExt as _};
use crate::{
- event::{Sequenced as _, routes::get},
+ event::Sequenced as _,
test::fixtures::{self, future::Expect as _},
};
@@ -31,11 +31,11 @@ async fn resume() {
let resume_at = {
// First subscription
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber.clone(),
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -52,11 +52,11 @@ async fn resume() {
};
// Resume after disconnect
- let get::Response(resumed) = get::handler(
+ let super::Response(resumed) = super::handler(
State(app),
subscriber,
Some(resume_at.into()),
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -113,11 +113,11 @@ async fn serial_resume() {
// First subscription
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber.clone(),
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -156,11 +156,11 @@ async fn serial_resume() {
];
// Second subscription
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber.clone(),
Some(resume_at.into()),
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -199,11 +199,11 @@ async fn serial_resume() {
];
// Third subscription
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber.clone(),
Some(resume_at.into()),
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
diff --git a/src/event/routes/test/setup.rs b/src/event/handlers/stream/test/setup.rs
index 1170fe4..5335055 100644
--- a/src/event/routes/test/setup.rs
+++ b/src/event/handlers/stream/test/setup.rs
@@ -2,10 +2,7 @@ use axum::extract::State;
use axum_extra::extract::Query;
use futures::{future, stream::StreamExt as _};
-use crate::{
- event::routes::get,
- test::fixtures::{self, future::Expect as _},
-};
+use crate::test::fixtures::{self, future::Expect as _};
// There's no test for this in subscribe-then-setup order because creating an
// identity to subscribe with also completes initial setup, preventing the
@@ -29,11 +26,11 @@ async fn previously_completed() {
// Subscribe to events
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
diff --git a/src/event/routes/test/token.rs b/src/event/handlers/stream/test/token.rs
index a467de5..2008323 100644
--- a/src/event/routes/test/token.rs
+++ b/src/event/handlers/stream/test/token.rs
@@ -2,10 +2,7 @@ use axum::extract::State;
use axum_extra::extract::Query;
use futures::{future, stream::StreamExt as _};
-use crate::{
- event::routes::get,
- test::fixtures::{self, future::Expect as _},
-};
+use crate::test::fixtures::{self, future::Expect as _};
#[tokio::test]
async fn terminates_on_token_expiry() {
@@ -22,11 +19,11 @@ async fn terminates_on_token_expiry() {
let subscriber =
fixtures::identity::logged_in(&app, &subscriber_creds, &fixtures::ancient()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber,
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -67,11 +64,11 @@ async fn terminates_on_logout() {
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber.clone(),
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
@@ -115,11 +112,11 @@ async fn terminates_on_password_change() {
let cookie = fixtures::cookie::logged_in(&app, &creds, &fixtures::now()).await;
let subscriber = fixtures::identity::from_cookie(&app, &cookie, &fixtures::now()).await;
- let get::Response(events) = get::handler(
+ let super::Response(events) = super::handler(
State(app.clone()),
subscriber.clone(),
None,
- Query(get::QueryParams { resume_point }),
+ Query(super::QueryParams { resume_point }),
)
.await
.expect("subscribe never fails");
diff --git a/src/event/mod.rs b/src/event/mod.rs
index 1f2ec42..6657243 100644
--- a/src/event/mod.rs
+++ b/src/event/mod.rs
@@ -1,18 +1,18 @@
-use crate::{channel, message, user};
-use axum::response::sse;
-use axum::response::sse::KeepAlive;
use std::time::Duration;
+use axum::response::sse::{self, KeepAlive};
+
+use crate::{channel, message, user};
+
pub mod app;
mod broadcaster;
mod extract;
+pub mod handlers;
pub mod repo;
-mod routes;
mod sequence;
pub use self::{
broadcaster::Broadcaster,
- routes::router,
sequence::{Instant, Sequence, Sequenced},
};
diff --git a/src/event/routes/mod.rs b/src/event/routes/mod.rs
deleted file mode 100644
index 742d397..0000000
--- a/src/event/routes/mod.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use axum::{Router, routing::get};
-
-use crate::app::App;
-
-mod get;
-#[cfg(test)]
-mod test;
-
-pub fn router() -> Router<App> {
- Router::new().route("/api/events", get(get::handler))
-}
diff --git a/src/invite/routes/invite/post.rs b/src/invite/handlers/accept/mod.rs
index 58d15c2..0d5f08a 100644
--- a/src/invite/routes/invite/post.rs
+++ b/src/invite/handlers/accept/mod.rs
@@ -8,17 +8,20 @@ use crate::{
app::App,
clock::RequestedAt,
error::{Internal, NotFound},
- invite::app,
+ invite::{app, handlers::PathInfo},
name::Name,
token::extract::IdentityCookie,
user::{Password, User},
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
RequestedAt(accepted_at): RequestedAt,
identity: IdentityCookie,
- Path(invite): Path<super::PathInfo>,
+ Path(invite): Path<PathInfo>,
Json(request): Json<Request>,
) -> Result<(IdentityCookie, Json<User>), Error> {
let (login, secret) = app
diff --git a/src/invite/routes/invite/test/post.rs b/src/invite/handlers/accept/test.rs
index b204b32..cb13900 100644
--- a/src/invite/routes/invite/test/post.rs
+++ b/src/invite/handlers/accept/test.rs
@@ -1,10 +1,6 @@
use axum::extract::{Json, Path, State};
-use crate::{
- invite::{app::AcceptError, routes::invite::post},
- name::Name,
- test::fixtures,
-};
+use crate::{invite::app::AcceptError, name::Name, test::fixtures};
#[tokio::test]
async fn valid_invite() {
@@ -18,11 +14,11 @@ async fn valid_invite() {
let (name, password) = fixtures::user::propose();
let identity = fixtures::cookie::not_logged_in();
- let request = post::Request {
+ let request = super::Request {
name: name.clone(),
password: password.clone(),
};
- let (identity, Json(response)) = post::handler(
+ let (identity, Json(response)) = super::handler(
State(app.clone()),
fixtures::now(),
identity,
@@ -70,11 +66,11 @@ async fn nonexistent_invite() {
let (name, password) = fixtures::user::propose();
let identity = fixtures::cookie::not_logged_in();
- let request = post::Request {
+ let request = super::Request {
name: name.clone(),
password: password.clone(),
};
- let post::Error(error) = post::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
fixtures::now(),
identity,
@@ -106,11 +102,11 @@ async fn expired_invite() {
let (name, password) = fixtures::user::propose();
let identity = fixtures::cookie::not_logged_in();
- let request = post::Request {
+ let request = super::Request {
name: name.clone(),
password: password.clone(),
};
- let post::Error(error) = post::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
fixtures::now(),
identity,
@@ -143,11 +139,11 @@ async fn accepted_invite() {
let (name, password) = fixtures::user::propose();
let identity = fixtures::cookie::not_logged_in();
- let request = post::Request {
+ let request = super::Request {
name: name.clone(),
password: password.clone(),
};
- let post::Error(error) = post::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
fixtures::now(),
identity,
@@ -186,11 +182,11 @@ async fn conflicting_name() {
let password = fixtures::user::propose_password();
let identity = fixtures::cookie::not_logged_in();
- let request = post::Request {
+ let request = super::Request {
name: conflicting_name.clone(),
password: password.clone(),
};
- let post::Error(error) = post::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
fixtures::now(),
identity,
@@ -220,11 +216,11 @@ async fn invalid_name() {
let name = fixtures::user::propose_invalid_name();
let password = fixtures::user::propose_password();
let identity = fixtures::cookie::not_logged_in();
- let request = post::Request {
+ let request = super::Request {
name: name.clone(),
password: password.clone(),
};
- let post::Error(error) = post::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
fixtures::now(),
identity,
diff --git a/src/invite/routes/invite/get.rs b/src/invite/handlers/get/mod.rs
index f862833..bb72586 100644
--- a/src/invite/routes/invite/get.rs
+++ b/src/invite/handlers/get/mod.rs
@@ -6,12 +6,15 @@ use axum::{
use crate::{
app::App,
error::{Internal, NotFound},
- invite::{Id, Summary},
+ invite::{Id, Summary, handlers::PathInfo},
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
- Path(invite): Path<super::PathInfo>,
+ Path(invite): Path<PathInfo>,
) -> Result<Json<Summary>, Error> {
app.invites()
.get(&invite)
diff --git a/src/invite/routes/invite/test/get.rs b/src/invite/handlers/get/test.rs
index 0dc8a79..0f2f725 100644
--- a/src/invite/routes/invite/test/get.rs
+++ b/src/invite/handlers/get/test.rs
@@ -1,6 +1,6 @@
use axum::extract::{Json, Path, State};
-use crate::{invite::routes::invite::get, test::fixtures};
+use crate::test::fixtures;
#[tokio::test]
async fn valid_invite() {
@@ -12,7 +12,7 @@ async fn valid_invite() {
// Call endpoint
- let Json(response) = get::handler(State(app), Path(invite.id))
+ let Json(response) = super::handler(State(app), Path(invite.id))
.await
.expect("get for an existing invite succeeds");
@@ -31,13 +31,13 @@ async fn nonexistent_invite() {
// Call endpoint
let invite = fixtures::invite::fictitious();
- let error = get::handler(State(app), Path(invite.clone()))
+ let error = super::handler(State(app), Path(invite.clone()))
.await
.expect_err("get for a nonexistent invite fails");
// Verify response
- assert!(matches!(error, get::Error::NotFound(error_id) if invite == error_id));
+ assert!(matches!(error, super::Error::NotFound(error_id) if invite == error_id));
}
#[tokio::test]
@@ -55,11 +55,11 @@ async fn expired_invite() {
// Call endpoint
- let error = get::handler(State(app), Path(invite.id.clone()))
+ let error = super::handler(State(app), Path(invite.id.clone()))
.await
.expect_err("get for an expired invite fails");
// Verify response
- assert!(matches!(error, get::Error::NotFound(error_id) if invite.id == error_id));
+ assert!(matches!(error, super::Error::NotFound(error_id) if invite.id == error_id));
}
diff --git a/src/invite/routes/post.rs b/src/invite/handlers/issue/mod.rs
index f7ca76c..6085f7a 100644
--- a/src/invite/routes/post.rs
+++ b/src/invite/handlers/issue/mod.rs
@@ -4,6 +4,9 @@ use crate::{
app::App, clock::RequestedAt, error::Internal, invite::Invite, token::extract::Identity,
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
RequestedAt(issued_at): RequestedAt,
diff --git a/src/invite/routes/test.rs b/src/invite/handlers/issue/test.rs
index 4ea8a3d..2bf5400 100644
--- a/src/invite/routes/test.rs
+++ b/src/invite/handlers/issue/test.rs
@@ -1,6 +1,5 @@
use axum::extract::{Json, State};
-use super::post;
use crate::test::fixtures;
#[tokio::test]
@@ -13,11 +12,11 @@ async fn create_invite() {
// Call the endpoint
- let Json(invite) = post::handler(
+ let Json(invite) = super::handler(
State(app),
issued_at.clone(),
issuer.clone(),
- Json(post::Request {}),
+ Json(super::Request {}),
)
.await
.expect("creating an invite always succeeds");
diff --git a/src/invite/handlers/mod.rs b/src/invite/handlers/mod.rs
new file mode 100644
index 0000000..07a2bbe
--- /dev/null
+++ b/src/invite/handlers/mod.rs
@@ -0,0 +1,9 @@
+mod accept;
+mod get;
+mod issue;
+
+type PathInfo = crate::invite::Id;
+
+pub use accept::handler as accept;
+pub use get::handler as get;
+pub use issue::handler as issue;
diff --git a/src/invite/mod.rs b/src/invite/mod.rs
index 2d32fda..74b9ceb 100644
--- a/src/invite/mod.rs
+++ b/src/invite/mod.rs
@@ -1,11 +1,11 @@
+use crate::{clock::DateTime, normalize::nfc, user};
+
pub mod app;
+pub mod handlers;
mod id;
mod repo;
-mod routes;
-
-use crate::{clock::DateTime, normalize::nfc, user};
-pub use self::{id::Id, routes::router};
+pub use self::id::Id;
#[derive(Debug, serde::Serialize)]
pub struct Invite {
diff --git a/src/invite/routes/invite/mod.rs b/src/invite/routes/invite/mod.rs
deleted file mode 100644
index c22029a..0000000
--- a/src/invite/routes/invite/mod.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-pub mod get;
-pub mod post;
-#[cfg(test)]
-pub mod test;
-
-type PathInfo = crate::invite::Id;
diff --git a/src/invite/routes/invite/test/mod.rs b/src/invite/routes/invite/test/mod.rs
deleted file mode 100644
index d6c1f06..0000000
--- a/src/invite/routes/invite/test/mod.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-mod get;
-mod post;
diff --git a/src/invite/routes/mod.rs b/src/invite/routes/mod.rs
deleted file mode 100644
index d83efc6..0000000
--- a/src/invite/routes/mod.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use axum::{
- Router,
- routing::{get, post},
-};
-
-use crate::app::App;
-
-mod invite;
-mod post;
-#[cfg(test)]
-mod test;
-
-pub fn router() -> Router<App> {
- Router::new()
- .route("/api/invite", post(post::handler))
- .route("/api/invite/{invite}", get(invite::get::handler))
- .route("/api/invite/{invite}", post(invite::post::handler))
-}
diff --git a/src/lib.rs b/src/lib.rs
index 4cce63b..d2fa390 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -17,6 +17,7 @@ mod invite;
mod message;
mod name;
mod normalize;
+mod routes;
mod setup;
#[cfg(test)]
mod test;
diff --git a/src/message/handlers/delete/mod.rs b/src/message/handlers/delete/mod.rs
new file mode 100644
index 0000000..5eac4eb
--- /dev/null
+++ b/src/message/handlers/delete/mod.rs
@@ -0,0 +1,55 @@
+use axum::{
+ extract::{Json, Path, State},
+ http::StatusCode,
+ response::{self, IntoResponse},
+};
+
+use crate::{
+ app::App,
+ clock::RequestedAt,
+ error::{Internal, NotFound},
+ message::{self, app::DeleteError},
+ token::extract::Identity,
+};
+
+#[cfg(test)]
+mod test;
+
+pub async fn handler(
+ State(app): State<App>,
+ Path(message): Path<message::Id>,
+ RequestedAt(deleted_at): RequestedAt,
+ identity: Identity,
+) -> Result<Response, Error> {
+ app.messages()
+ .delete(&identity.user, &message, &deleted_at)
+ .await?;
+
+ Ok(Response { id: message })
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct Response {
+ pub id: message::Id,
+}
+
+impl IntoResponse for Response {
+ fn into_response(self) -> response::Response {
+ (StatusCode::ACCEPTED, Json(self)).into_response()
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error(transparent)]
+pub struct Error(#[from] pub DeleteError);
+
+impl IntoResponse for Error {
+ fn into_response(self) -> response::Response {
+ let Self(error) = self;
+ match error {
+ DeleteError::NotSender(_) => (StatusCode::FORBIDDEN, error.to_string()).into_response(),
+ DeleteError::NotFound(_) | DeleteError::Deleted(_) => NotFound(error).into_response(),
+ DeleteError::Database(_) => Internal::from(error).into_response(),
+ }
+ }
+}
diff --git a/src/message/routes/message/test.rs b/src/message/handlers/delete/test.rs
index 1888be7..15aa2c2 100644
--- a/src/message/routes/message/test.rs
+++ b/src/message/handlers/delete/test.rs
@@ -1,6 +1,5 @@
use axum::extract::{Path, State};
-use super::delete;
use crate::{message::app, test::fixtures};
#[tokio::test]
@@ -14,7 +13,7 @@ pub async fn delete_message() {
// Send the request
- let response = delete::handler(
+ let response = super::handler(
State(app.clone()),
Path(message.id.clone()),
fixtures::now(),
@@ -43,7 +42,7 @@ pub async fn delete_invalid_message_id() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let message = fixtures::message::fictitious();
- let delete::Error(error) = delete::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
Path(message.clone()),
fixtures::now(),
@@ -74,7 +73,7 @@ pub async fn delete_deleted() {
// Send the request
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
- let delete::Error(error) = delete::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
Path(message.id.clone()),
fixtures::now(),
@@ -105,7 +104,7 @@ pub async fn delete_expired() {
// Send the request
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
- let delete::Error(error) = delete::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
Path(message.id.clone()),
fixtures::now(),
@@ -141,7 +140,7 @@ pub async fn delete_purged() {
// Send the request
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
- let delete::Error(error) = delete::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
Path(message.id.clone()),
fixtures::now(),
@@ -167,7 +166,7 @@ pub async fn delete_not_sender() {
// Send the request
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
- let delete::Error(error) = delete::handler(
+ let super::Error(error) = super::handler(
State(app.clone()),
Path(message.id.clone()),
fixtures::now(),
diff --git a/src/message/handlers/mod.rs b/src/message/handlers/mod.rs
new file mode 100644
index 0000000..7e78475
--- /dev/null
+++ b/src/message/handlers/mod.rs
@@ -0,0 +1,3 @@
+mod delete;
+
+pub use delete::handler as delete;
diff --git a/src/message/mod.rs b/src/message/mod.rs
index c2687bc..e1643e6 100644
--- a/src/message/mod.rs
+++ b/src/message/mod.rs
@@ -1,12 +1,10 @@
pub mod app;
mod body;
pub mod event;
+pub mod handlers;
mod history;
mod id;
pub mod repo;
-mod routes;
mod snapshot;
-pub use self::{
- body::Body, event::Event, history::History, id::Id, routes::router, snapshot::Message,
-};
+pub use self::{body::Body, event::Event, history::History, id::Id, snapshot::Message};
diff --git a/src/message/routes/message/mod.rs b/src/message/routes/message/mod.rs
deleted file mode 100644
index a05d344..0000000
--- a/src/message/routes/message/mod.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-#[cfg(test)]
-mod test;
-
-pub mod delete {
- use axum::{
- extract::{Json, Path, State},
- http::StatusCode,
- response::{self, IntoResponse},
- };
-
- use crate::{
- app::App,
- clock::RequestedAt,
- error::{Internal, NotFound},
- message::{self, app::DeleteError},
- token::extract::Identity,
- };
-
- pub async fn handler(
- State(app): State<App>,
- Path(message): Path<message::Id>,
- RequestedAt(deleted_at): RequestedAt,
- identity: Identity,
- ) -> Result<Response, Error> {
- app.messages()
- .delete(&identity.user, &message, &deleted_at)
- .await?;
-
- Ok(Response { id: message })
- }
-
- #[derive(Debug, serde::Serialize)]
- pub struct Response {
- pub id: message::Id,
- }
-
- impl IntoResponse for Response {
- fn into_response(self) -> response::Response {
- (StatusCode::ACCEPTED, Json(self)).into_response()
- }
- }
-
- #[derive(Debug, thiserror::Error)]
- #[error(transparent)]
- pub struct Error(#[from] pub DeleteError);
-
- impl IntoResponse for Error {
- fn into_response(self) -> response::Response {
- let Self(error) = self;
- match error {
- DeleteError::NotSender(_) => {
- (StatusCode::FORBIDDEN, error.to_string()).into_response()
- }
- DeleteError::NotFound(_) | DeleteError::Deleted(_) => {
- NotFound(error).into_response()
- }
- DeleteError::Database(_) => Internal::from(error).into_response(),
- }
- }
- }
-}
diff --git a/src/message/routes/mod.rs b/src/message/routes/mod.rs
deleted file mode 100644
index 00b2b1a..0000000
--- a/src/message/routes/mod.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use axum::{Router, routing::delete};
-
-use crate::app::App;
-
-mod message;
-
-pub fn router() -> Router<App> {
- Router::new().route("/api/messages/{message}", delete(message::delete::handler))
-}
diff --git a/src/routes.rs b/src/routes.rs
new file mode 100644
index 0000000..1e66582
--- /dev/null
+++ b/src/routes.rs
@@ -0,0 +1,62 @@
+use axum::{
+ Router, middleware,
+ response::Redirect,
+ routing::{delete, get, post},
+};
+
+use crate::{app::App, boot, channel, event, expire, invite, message, setup, ui, user};
+
+pub fn routes(app: &App) -> Router<App> {
+ // UI routes that can be accessed before the administrator completes setup.
+ let ui_bootstrap = Router::new()
+ .route("/{*path}", get(ui::handlers::asset))
+ .route("/setup", get(ui::handlers::setup));
+
+ // UI routes that require the administrator to complete setup first.
+ let ui_setup_required = Router::new()
+ .route("/", get(ui::handlers::index))
+ .route("/ch/{channel}", get(ui::handlers::channel))
+ .route("/invite/{invite}", get(ui::handlers::invite))
+ .route("/login", get(ui::handlers::login))
+ .route("/me", get(ui::handlers::me))
+ .route_layer(crate::setup::Required(app.clone()).with_fallback(Redirect::to("/setup")));
+
+ // API routes that can run before the administrator completes setup.
+ let api_bootstrap = Router::new().route("/api/setup", post(setup::handlers::setup));
+
+ // API routes that require the administrator to complete setup first.
+ let api_setup_required = Router::new()
+ .route("/api/auth/login", post(user::handlers::login))
+ .route("/api/auth/logout", post(user::handlers::logout))
+ .route("/api/boot", get(boot::handlers::boot))
+ .route("/api/channels", post(channel::handlers::create))
+ .route("/api/channels/{channel}", post(channel::handlers::send))
+ .route("/api/channels/{channel}", delete(channel::handlers::delete))
+ .route("/api/events", get(event::handlers::stream))
+ .route("/api/invite", post(invite::handlers::issue))
+ .route("/api/invite/{invite}", get(invite::handlers::get))
+ .route("/api/invite/{invite}", post(invite::handlers::accept))
+ .route("/api/messages/{message}", delete(message::handlers::delete))
+ .route("/api/password", post(user::handlers::change_password))
+ // Run expiry whenever someone accesses the API. This was previously a blanket middleware
+ // affecting the whole service, but loading the client makes a several requests before the
+ // client can completely load, each of which was triggering expiry. There is absolutely no
+ // upside to re-checking expiry tens of times back-to-back like that; the API is accessed
+ // more regularly and with less of a traffic rush.
+ //
+ // This should, probably, be moved to a background job at some point.
+ .route_layer(middleware::from_fn_with_state(
+ app.clone(),
+ expire::middleware,
+ ))
+ .route_layer(setup::Required(app.clone()));
+
+ [
+ ui_bootstrap,
+ ui_setup_required,
+ api_bootstrap,
+ api_setup_required,
+ ]
+ .into_iter()
+ .fold(Router::default(), Router::merge)
+}
diff --git a/src/setup/handlers/mod.rs b/src/setup/handlers/mod.rs
new file mode 100644
index 0000000..3d4a4e6
--- /dev/null
+++ b/src/setup/handlers/mod.rs
@@ -0,0 +1,3 @@
+mod setup;
+
+pub use setup::handler as setup;
diff --git a/src/setup/routes/post.rs b/src/setup/handlers/setup/mod.rs
index 0ff5d69..cbb3072 100644
--- a/src/setup/routes/post.rs
+++ b/src/setup/handlers/setup/mod.rs
@@ -14,6 +14,9 @@ use crate::{
user::{Password, User},
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
RequestedAt(setup_at): RequestedAt,
diff --git a/src/setup/routes/test.rs b/src/setup/handlers/setup/test.rs
index e9f5cd6..8243ac3 100644
--- a/src/setup/routes/test.rs
+++ b/src/setup/handlers/setup/test.rs
@@ -1,6 +1,5 @@
use axum::extract::{Json, State};
-use super::post;
use crate::{setup::app, test::fixtures};
#[tokio::test]
@@ -12,12 +11,12 @@ async fn fresh_instance() {
// Call the endpoint
let identity = fixtures::cookie::not_logged_in();
let (name, password) = fixtures::user::propose();
- let request = post::Request {
+ let request = super::Request {
name: name.clone(),
password: password.clone(),
};
let (identity, Json(response)) =
- post::handler(State(app.clone()), fixtures::now(), identity, Json(request))
+ super::handler(State(app.clone()), fixtures::now(), identity, Json(request))
.await
.expect("setup in a fresh app succeeds");
@@ -57,9 +56,9 @@ async fn login_exists() {
// Call the endpoint
let identity = fixtures::cookie::not_logged_in();
let (name, password) = fixtures::user::propose();
- let request = post::Request { name, password };
- let post::Error(error) =
- post::handler(State(app.clone()), fixtures::now(), identity, Json(request))
+ let request = super::Request { name, password };
+ let super::Error(error) =
+ super::handler(State(app.clone()), fixtures::now(), identity, Json(request))
.await
.expect_err("setup in a populated app fails");
@@ -79,12 +78,12 @@ async fn invalid_name() {
let name = fixtures::user::propose_invalid_name();
let password = fixtures::user::propose_password();
let identity = fixtures::cookie::not_logged_in();
- let request = post::Request {
+ let request = super::Request {
name: name.clone(),
password: password.clone(),
};
- let post::Error(error) =
- post::handler(State(app.clone()), fixtures::now(), identity, Json(request))
+ let super::Error(error) =
+ super::handler(State(app.clone()), fixtures::now(), identity, Json(request))
.await
.expect_err("setup with an invalid name fails");
diff --git a/src/setup/mod.rs b/src/setup/mod.rs
index a4b821c..f5d12df 100644
--- a/src/setup/mod.rs
+++ b/src/setup/mod.rs
@@ -1,6 +1,6 @@
pub mod app;
+pub mod handlers;
pub mod repo;
mod required;
-mod routes;
-pub use self::{required::Required, routes::router};
+pub use self::required::Required;
diff --git a/src/setup/routes/mod.rs b/src/setup/routes/mod.rs
deleted file mode 100644
index 977a790..0000000
--- a/src/setup/routes/mod.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use axum::{Router, routing::post};
-
-use crate::app::App;
-
-mod post;
-#[cfg(test)]
-mod test;
-
-pub fn router() -> Router<App> {
- Router::new().route("/api/setup", post(post::handler))
-}
diff --git a/src/ui/handlers/asset.rs b/src/ui/handlers/asset.rs
new file mode 100644
index 0000000..1d5b8be
--- /dev/null
+++ b/src/ui/handlers/asset.rs
@@ -0,0 +1,7 @@
+use axum::extract::Path;
+
+use crate::ui::assets::{Asset, Assets, Error};
+
+pub async fn handler(Path(path): Path<String>) -> Result<Asset, Error> {
+ Assets::load(path)
+}
diff --git a/src/ui/handlers/channel.rs b/src/ui/handlers/channel.rs
new file mode 100644
index 0000000..d3199dd
--- /dev/null
+++ b/src/ui/handlers/channel.rs
@@ -0,0 +1,58 @@
+use axum::{
+ extract::{Path, State},
+ response::{self, IntoResponse, Redirect},
+};
+
+use crate::{
+ app::App,
+ channel::{self, app},
+ error::Internal,
+ token::extract::Identity,
+ ui::{
+ assets::{Asset, Assets},
+ error::NotFound,
+ },
+};
+
+pub async fn handler(
+ State(app): State<App>,
+ identity: Option<Identity>,
+ Path(channel): Path<channel::Id>,
+) -> Result<Asset, Error> {
+ let _ = identity.ok_or(Error::NotLoggedIn)?;
+ app.channels().get(&channel).await.map_err(Error::from)?;
+
+ Assets::index().map_err(Error::Internal)
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error("channel not found")]
+ NotFound,
+ #[error("not logged in")]
+ NotLoggedIn,
+ #[error("{0}")]
+ Internal(Internal),
+}
+
+impl From<app::Error> for Error {
+ fn from(error: app::Error) -> Self {
+ match error {
+ app::Error::NotFound(_) | app::Error::Deleted(_) => Self::NotFound,
+ other => Self::Internal(other.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/get.rs b/src/ui/handlers/index.rs
index 2fcb51c..2fcb51c 100644
--- a/src/ui/routes/get.rs
+++ b/src/ui/handlers/index.rs
diff --git a/src/ui/handlers/invite.rs b/src/ui/handlers/invite.rs
new file mode 100644
index 0000000..0f9580a
--- /dev/null
+++ b/src/ui/handlers/invite.rs
@@ -0,0 +1,53 @@
+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<App>,
+ Path(invite): Path<invite::Id>,
+) -> Result<Asset, Error> {
+ 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<Internal>) -> 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/handlers/login.rs b/src/ui/handlers/login.rs
new file mode 100644
index 0000000..4562b04
--- /dev/null
+++ b/src/ui/handlers/login.rs
@@ -0,0 +1,8 @@
+use crate::{
+ error::Internal,
+ ui::assets::{Asset, Assets},
+};
+
+pub async fn handler() -> Result<Asset, Internal> {
+ Assets::index()
+}
diff --git a/src/ui/handlers/me.rs b/src/ui/handlers/me.rs
new file mode 100644
index 0000000..2fcb51c
--- /dev/null
+++ b/src/ui/handlers/me.rs
@@ -0,0 +1,30 @@
+use axum::response::{self, IntoResponse, Redirect};
+
+use crate::{
+ error::Internal,
+ token::extract::Identity,
+ ui::assets::{Asset, Assets},
+};
+
+pub async fn handler(identity: Option<Identity>) -> Result<Asset, Error> {
+ let _ = identity.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/handlers/mod.rs b/src/ui/handlers/mod.rs
new file mode 100644
index 0000000..5bfd0d6
--- /dev/null
+++ b/src/ui/handlers/mod.rs
@@ -0,0 +1,15 @@
+mod asset;
+mod channel;
+mod index;
+mod invite;
+mod login;
+mod me;
+mod setup;
+
+pub use asset::handler as asset;
+pub use channel::handler as channel;
+pub use index::handler as index;
+pub use invite::handler as invite;
+pub use login::handler as login;
+pub use me::handler as me;
+pub use setup::handler as setup;
diff --git a/src/ui/handlers/setup.rs b/src/ui/handlers/setup.rs
new file mode 100644
index 0000000..49821cf
--- /dev/null
+++ b/src/ui/handlers/setup.rs
@@ -0,0 +1,41 @@
+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<App>) -> Result<Asset, Error> {
+ 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(),
+ }
+ }
+}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index e834bba..9668300 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -1,6 +1,4 @@
mod assets;
mod error;
+pub mod handlers;
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
deleted file mode 100644
index a854f14..0000000
--- a/src/ui/routes/ch/channel.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-pub mod get {
- use axum::{
- extract::{Path, State},
- response::{self, IntoResponse, Redirect},
- };
-
- use crate::{
- app::App,
- channel::{self, app},
- error::Internal,
- token::extract::Identity,
- ui::{
- assets::{Asset, Assets},
- error::NotFound,
- },
- };
-
- pub async fn handler(
- State(app): State<App>,
- identity: Option<Identity>,
- Path(channel): Path<channel::Id>,
- ) -> Result<Asset, Error> {
- let _ = identity.ok_or(Error::NotLoggedIn)?;
- app.channels().get(&channel).await.map_err(Error::from)?;
-
- Assets::index().map_err(Error::Internal)
- }
-
- #[derive(Debug, thiserror::Error)]
- pub enum Error {
- #[error("channel not found")]
- NotFound,
- #[error("not logged in")]
- NotLoggedIn,
- #[error("{0}")]
- Internal(Internal),
- }
-
- impl From<app::Error> for Error {
- fn from(error: app::Error) -> Self {
- match error {
- app::Error::NotFound(_) | app::Error::Deleted(_) => Self::NotFound,
- other => Self::Internal(other.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
deleted file mode 100644
index ff02972..0000000
--- a/src/ui/routes/ch/mod.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod channel;
diff --git a/src/ui/routes/invite/invite.rs b/src/ui/routes/invite/invite.rs
deleted file mode 100644
index 06e5792..0000000
--- a/src/ui/routes/invite/invite.rs
+++ /dev/null
@@ -1,55 +0,0 @@
-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<App>,
- Path(invite): Path<invite::Id>,
- ) -> Result<Asset, Error> {
- 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<Internal>) -> 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
deleted file mode 100644
index 50af8be..0000000
--- a/src/ui/routes/invite/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-// 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
deleted file mode 100644
index 39d45b9..0000000
--- a/src/ui/routes/login.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-pub mod get {
- use crate::{
- error::Internal,
- ui::assets::{Asset, Assets},
- };
-
- pub async fn handler() -> Result<Asset, Internal> {
- Assets::index()
- }
-}
diff --git a/src/ui/routes/me.rs b/src/ui/routes/me.rs
deleted file mode 100644
index f1f118f..0000000
--- a/src/ui/routes/me.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-pub mod get {
- use axum::response::{self, IntoResponse, Redirect};
-
- use crate::{
- error::Internal,
- token::extract::Identity,
- ui::assets::{Asset, Assets},
- };
-
- pub async fn handler(identity: Option<Identity>) -> Result<Asset, Error> {
- let _ = identity.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/mod.rs b/src/ui/routes/mod.rs
deleted file mode 100644
index dc94773..0000000
--- a/src/ui/routes/mod.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-use axum::{Router, response::Redirect, routing::get};
-
-use crate::app::App;
-
-mod ch;
-mod get;
-mod invite;
-mod login;
-mod me;
-mod path;
-mod setup;
-
-pub fn router(app: &App) -> Router<App> {
- [
- Router::new()
- .route("/{*path}", get(path::get::handler))
- .route("/setup", get(setup::get::handler)),
- Router::new()
- .route("/", get(get::handler))
- .route("/me", get(me::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(crate::setup::Required(app.clone()).with_fallback(Redirect::to("/setup"))),
- ]
- .into_iter()
- .fold(Router::default(), Router::merge)
-}
diff --git a/src/ui/routes/path.rs b/src/ui/routes/path.rs
deleted file mode 100644
index a387552..0000000
--- a/src/ui/routes/path.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-pub mod get {
- use axum::extract::Path;
-
- use crate::ui::assets::{Asset, Assets, Error};
-
- pub async fn handler(Path(path): Path<String>) -> Result<Asset, Error> {
- Assets::load(path)
- }
-}
diff --git a/src/ui/routes/setup.rs b/src/ui/routes/setup.rs
deleted file mode 100644
index 649cc5f..0000000
--- a/src/ui/routes/setup.rs
+++ /dev/null
@@ -1,43 +0,0 @@
-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<App>) -> Result<Asset, Error> {
- 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(),
- }
- }
- }
-}
diff --git a/src/user/routes/login/post.rs b/src/user/handlers/login/mod.rs
index 39f9eea..e80377e 100644
--- a/src/user/routes/login/post.rs
+++ b/src/user/handlers/login/mod.rs
@@ -13,6 +13,9 @@ use crate::{
user::{Password, User},
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
RequestedAt(now): RequestedAt,
diff --git a/src/user/routes/login/test.rs b/src/user/handlers/login/test.rs
index d2e7ee2..b8f24f6 100644
--- a/src/user/routes/login/test.rs
+++ b/src/user/handlers/login/test.rs
@@ -1,6 +1,5 @@
use axum::extract::{Json, State};
-use super::post;
use crate::{test::fixtures, token::app};
#[tokio::test]
@@ -14,12 +13,12 @@ async fn correct_credentials() {
let identity = fixtures::cookie::not_logged_in();
let logged_in_at = fixtures::now();
- let request = post::Request {
+ let request = super::Request {
name: name.clone(),
password,
};
let (identity, Json(response)) =
- post::handler(State(app.clone()), logged_in_at, identity, Json(request))
+ super::handler(State(app.clone()), logged_in_at, identity, Json(request))
.await
.expect("logged in with valid credentials");
@@ -53,12 +52,12 @@ async fn invalid_name() {
let identity = fixtures::cookie::not_logged_in();
let logged_in_at = fixtures::now();
let (name, password) = fixtures::user::propose();
- let request = post::Request {
+ let request = super::Request {
name: name.clone(),
password,
};
- let post::Error(error) =
- post::handler(State(app.clone()), logged_in_at, identity, Json(request))
+ let super::Error(error) =
+ super::handler(State(app.clone()), logged_in_at, identity, Json(request))
.await
.expect_err("logged in with an incorrect password fails");
@@ -78,12 +77,12 @@ async fn incorrect_password() {
let logged_in_at = fixtures::now();
let identity = fixtures::cookie::not_logged_in();
- let request = post::Request {
+ let request = super::Request {
name: login.name,
password: fixtures::user::propose_password(),
};
- let post::Error(error) =
- post::handler(State(app.clone()), logged_in_at, identity, Json(request))
+ let super::Error(error) =
+ super::handler(State(app.clone()), logged_in_at, identity, Json(request))
.await
.expect_err("logged in with an incorrect password");
@@ -103,8 +102,8 @@ async fn token_expires() {
let logged_in_at = fixtures::ancient();
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))
+ let request = super::Request { name, password };
+ let (identity, _) = super::handler(State(app.clone()), logged_in_at, identity, Json(request))
.await
.expect("logged in with valid credentials");
let secret = identity.secret().expect("logged in with valid credentials");
diff --git a/src/user/routes/logout/post.rs b/src/user/handlers/logout/mod.rs
index 0ac663e..45a376a 100644
--- a/src/user/routes/logout/post.rs
+++ b/src/user/handlers/logout/mod.rs
@@ -11,6 +11,9 @@ use crate::{
token::{app, extract::IdentityCookie},
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
RequestedAt(now): RequestedAt,
diff --git a/src/user/routes/logout/test.rs b/src/user/handlers/logout/test.rs
index ce93760..8dc4636 100644
--- a/src/user/routes/logout/test.rs
+++ b/src/user/handlers/logout/test.rs
@@ -3,7 +3,6 @@ use axum::{
http::StatusCode,
};
-use super::post;
use crate::{test::fixtures, token::app};
#[tokio::test]
@@ -18,7 +17,7 @@ async fn successful() {
// Call the endpoint
- let (response_identity, response_status) = post::handler(
+ let (response_identity, response_status) = super::handler(
State(app.clone()),
fixtures::now(),
identity.clone(),
@@ -50,7 +49,7 @@ async fn no_identity() {
// Call the endpoint
let identity = fixtures::cookie::not_logged_in();
- let (identity, status) = post::handler(State(app), fixtures::now(), identity, Json::default())
+ let (identity, status) = super::handler(State(app), fixtures::now(), identity, Json::default())
.await
.expect("logged out with no token succeeds");
@@ -69,9 +68,10 @@ async fn invalid_token() {
// Call the endpoint
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 fails");
+ let super::Error(error) =
+ super::handler(State(app), fixtures::now(), identity, Json::default())
+ .await
+ .expect_err("logged out with an invalid token fails");
// Verify the return value's basic structure
diff --git a/src/user/handlers/mod.rs b/src/user/handlers/mod.rs
new file mode 100644
index 0000000..5cadbb5
--- /dev/null
+++ b/src/user/handlers/mod.rs
@@ -0,0 +1,7 @@
+mod login;
+mod logout;
+mod password;
+
+pub use login::handler as login;
+pub use logout::handler as logout;
+pub use password::handler as change_password;
diff --git a/src/user/routes/password/post.rs b/src/user/handlers/password/mod.rs
index 296f6cd..9158325 100644
--- a/src/user/routes/password/post.rs
+++ b/src/user/handlers/password/mod.rs
@@ -15,6 +15,9 @@ use crate::{
user::{Password, User},
};
+#[cfg(test)]
+mod test;
+
pub async fn handler(
State(app): State<App>,
RequestedAt(now): RequestedAt,
diff --git a/src/user/routes/password/test.rs b/src/user/handlers/password/test.rs
index f977327..42e41d8 100644
--- a/src/user/routes/password/test.rs
+++ b/src/user/handlers/password/test.rs
@@ -1,6 +1,5 @@
use axum::extract::{Json, State};
-use super::post;
use crate::{
test::fixtures,
token::app::{LoginError, ValidateError},
@@ -17,11 +16,11 @@ async fn password_change() {
// Call the endpoint
let (name, password) = creds;
let to = fixtures::user::propose_password();
- let request = post::Request {
+ let request = super::Request {
password: password.clone(),
to: to.clone(),
};
- let (new_cookie, Json(response)) = post::handler(
+ let (new_cookie, Json(response)) = super::handler(
State(app.clone()),
fixtures::now(),
identity.clone(),
diff --git a/src/user/mod.rs b/src/user/mod.rs
index f4c66ab..44e1633 100644
--- a/src/user/mod.rs
+++ b/src/user/mod.rs
@@ -2,14 +2,12 @@
pub mod app;
pub mod create;
pub mod event;
+pub mod handlers;
mod history;
mod id;
pub mod password;
pub mod repo;
-mod routes;
mod snapshot;
mod validate;
-pub use self::{
- event::Event, history::History, id::Id, password::Password, routes::router, snapshot::User,
-};
+pub use self::{event::Event, history::History, id::Id, password::Password, snapshot::User};
diff --git a/src/user/routes/login/mod.rs b/src/user/routes/login/mod.rs
deleted file mode 100644
index 36b384e..0000000
--- a/src/user/routes/login/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-pub mod post;
-
-#[cfg(test)]
-mod test;
diff --git a/src/user/routes/logout/mod.rs b/src/user/routes/logout/mod.rs
deleted file mode 100644
index 36b384e..0000000
--- a/src/user/routes/logout/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-pub mod post;
-
-#[cfg(test)]
-mod test;
diff --git a/src/user/routes/mod.rs b/src/user/routes/mod.rs
deleted file mode 100644
index ade96cb..0000000
--- a/src/user/routes/mod.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use axum::{Router, routing::post};
-
-use crate::app::App;
-
-mod login;
-mod logout;
-mod password;
-
-pub fn router() -> Router<App> {
- Router::new()
- .route("/api/password", post(password::post::handler))
- .route("/api/auth/login", post(login::post::handler))
- .route("/api/auth/logout", post(logout::post::handler))
-}
diff --git a/src/user/routes/password/mod.rs b/src/user/routes/password/mod.rs
deleted file mode 100644
index 36b384e..0000000
--- a/src/user/routes/password/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-pub mod post;
-
-#[cfg(test)]
-mod test;