summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.sqlx/query-8b474c8ed7859f745888644db639b7a4a21210ebf0b7fba97cd016ff6ab4d769.json (renamed from .sqlx/query-e0deb4dfaffe4527ad630c884970c3eb93fc734d979e6b8e78cd7d0b6dd0b669.json)6
-rw-r--r--src/app.rs2
-rw-r--r--src/channel/app.rs9
-rw-r--r--src/channel/mod.rs14
-rw-r--r--src/channel/routes.rs6
-rw-r--r--src/channel/routes/test/on_create.rs2
-rw-r--r--src/channel/routes/test/on_send.rs2
-rw-r--r--src/cli.rs4
-rw-r--r--src/event/app.rs (renamed from src/events/app.rs)9
-rw-r--r--src/event/broadcaster.rs (renamed from src/events/broadcaster.rs)2
-rw-r--r--src/event/extract.rs (renamed from src/events/extract.rs)0
-rw-r--r--src/event/mod.rs (renamed from src/events/mod.rs)3
-rw-r--r--src/event/repo/message.rs (renamed from src/events/repo/message.rs)8
-rw-r--r--src/event/repo/mod.rs (renamed from src/events/repo/mod.rs)0
-rw-r--r--src/event/routes.rs (renamed from src/events/routes.rs)5
-rw-r--r--src/event/routes/test.rs (renamed from src/events/routes/test.rs)2
-rw-r--r--src/event/sequence.rs24
-rw-r--r--src/event/types.rs (renamed from src/events/types.rs)7
-rw-r--r--src/lib.rs4
-rw-r--r--src/login/app.rs17
-rw-r--r--src/login/extract.rs181
-rw-r--r--src/login/mod.rs18
-rw-r--r--src/login/password.rs (renamed from src/password.rs)0
-rw-r--r--src/login/repo/auth.rs2
-rw-r--r--src/login/routes.rs6
-rw-r--r--src/login/token/mod.rs3
-rw-r--r--src/login/types.rs2
-rw-r--r--src/message/mod.rs6
-rw-r--r--src/repo/channel.rs15
-rw-r--r--src/repo/login.rs (renamed from src/repo/login/store.rs)15
-rw-r--r--src/repo/login/extract.rs15
-rw-r--r--src/repo/login/mod.rs4
-rw-r--r--src/repo/message.rs7
-rw-r--r--src/repo/mod.rs1
-rw-r--r--src/repo/sequence.rs27
-rw-r--r--src/repo/token.rs10
-rw-r--r--src/test/fixtures/channel.rs2
-rw-r--r--src/test/fixtures/filter.rs2
-rw-r--r--src/test/fixtures/identity.rs9
-rw-r--r--src/test/fixtures/login.rs3
-rw-r--r--src/test/fixtures/message.rs7
-rw-r--r--src/token/extract/identity.rs75
-rw-r--r--src/token/extract/identity_token.rs94
-rw-r--r--src/token/extract/mod.rs4
-rw-r--r--src/token/id.rs (renamed from src/login/token/id.rs)0
-rw-r--r--src/token/mod.rs5
-rw-r--r--src/token/secret.rs27
47 files changed, 336 insertions, 330 deletions
diff --git a/.sqlx/query-e0deb4dfaffe4527ad630c884970c3eb93fc734d979e6b8e78cd7d0b6dd0b669.json b/.sqlx/query-8b474c8ed7859f745888644db639b7a4a21210ebf0b7fba97cd016ff6ab4d769.json
index eda6697..b433e4c 100644
--- a/.sqlx/query-e0deb4dfaffe4527ad630c884970c3eb93fc734d979e6b8e78cd7d0b6dd0b669.json
+++ b/.sqlx/query-8b474c8ed7859f745888644db639b7a4a21210ebf0b7fba97cd016ff6ab4d769.json
@@ -1,10 +1,10 @@
{
"db_name": "SQLite",
- "query": "\n insert\n into token (id, secret, login, issued_at, last_used_at)\n values ($1, $2, $3, $4, $4)\n returning secret as \"secret!: IdentitySecret\"\n ",
+ "query": "\n insert\n into token (id, secret, login, issued_at, last_used_at)\n values ($1, $2, $3, $4, $4)\n returning secret as \"secret!: Secret\"\n ",
"describe": {
"columns": [
{
- "name": "secret!: IdentitySecret",
+ "name": "secret!: Secret",
"ordinal": 0,
"type_info": "Text"
}
@@ -16,5 +16,5 @@
false
]
},
- "hash": "e0deb4dfaffe4527ad630c884970c3eb93fc734d979e6b8e78cd7d0b6dd0b669"
+ "hash": "8b474c8ed7859f745888644db639b7a4a21210ebf0b7fba97cd016ff6ab4d769"
}
diff --git a/src/app.rs b/src/app.rs
index c13f52f..84a6357 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2,7 +2,7 @@ use sqlx::sqlite::SqlitePool;
use crate::{
channel::app::Channels,
- events::{app::Events, broadcaster::Broadcaster as EventBroadcaster},
+ event::{app::Events, broadcaster::Broadcaster as EventBroadcaster},
login::{app::Logins, broadcaster::Broadcaster as LoginBroadcaster},
};
diff --git a/src/channel/app.rs b/src/channel/app.rs
index d89e733..1422651 100644
--- a/src/channel/app.rs
+++ b/src/channel/app.rs
@@ -2,12 +2,11 @@ use chrono::TimeDelta;
use sqlx::sqlite::SqlitePool;
use crate::{
+ channel::Channel,
clock::DateTime,
- events::{broadcaster::Broadcaster, types::ChannelEvent},
- repo::{
- channel::{Channel, Provider as _},
- sequence::{Provider as _, Sequence},
- },
+ event::Sequence,
+ event::{broadcaster::Broadcaster, types::ChannelEvent},
+ repo::{channel::Provider as _, sequence::Provider as _},
};
pub struct Channels<'a> {
diff --git a/src/channel/mod.rs b/src/channel/mod.rs
index 3115e98..02d0ed4 100644
--- a/src/channel/mod.rs
+++ b/src/channel/mod.rs
@@ -1,7 +1,17 @@
+use crate::{clock::DateTime, event::Sequence};
+
pub mod app;
mod id;
mod routes;
-pub use self::routes::router;
+pub use self::{id::Id, routes::router};
-pub use self::id::Id;
+#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
+pub struct Channel {
+ pub id: Id,
+ pub name: String,
+ #[serde(skip)]
+ pub created_at: DateTime,
+ #[serde(skip)]
+ pub created_sequence: Sequence,
+}
diff --git a/src/channel/routes.rs b/src/channel/routes.rs
index 72d6195..5d8b61e 100644
--- a/src/channel/routes.rs
+++ b/src/channel/routes.rs
@@ -10,11 +10,11 @@ use axum_extra::extract::Query;
use super::app;
use crate::{
app::App,
- channel,
+ channel::{self, Channel},
clock::RequestedAt,
error::Internal,
- events::app::EventsError,
- repo::{channel::Channel, login::Login, sequence::Sequence},
+ event::{app::EventsError, Sequence},
+ login::Login,
};
#[cfg(test)]
diff --git a/src/channel/routes/test/on_create.rs b/src/channel/routes/test/on_create.rs
index 72980ac..9988932 100644
--- a/src/channel/routes/test/on_create.rs
+++ b/src/channel/routes/test/on_create.rs
@@ -3,7 +3,7 @@ use futures::stream::StreamExt as _;
use crate::{
channel::{app, routes},
- events::types,
+ event::types,
test::fixtures::{self, future::Immediately as _},
};
diff --git a/src/channel/routes/test/on_send.rs b/src/channel/routes/test/on_send.rs
index 987784d..6f844cd 100644
--- a/src/channel/routes/test/on_send.rs
+++ b/src/channel/routes/test/on_send.rs
@@ -4,7 +4,7 @@ use futures::stream::StreamExt;
use crate::{
channel,
channel::routes,
- events::{app, types},
+ event::{app, types},
test::fixtures::{self, future::Immediately as _},
};
diff --git a/src/cli.rs b/src/cli.rs
index 132baf8..ee95ea6 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -10,7 +10,7 @@ use clap::Parser;
use sqlx::sqlite::SqlitePool;
use tokio::net;
-use crate::{app::App, channel, clock, events, expire, login, repo::pool};
+use crate::{app::App, channel, clock, event, expire, login, repo::pool};
/// Command-line entry point for running the `hi` server.
///
@@ -105,7 +105,7 @@ impl Args {
}
fn routers() -> Router<App> {
- [channel::router(), events::router(), login::router()]
+ [channel::router(), event::router(), login::router()]
.into_iter()
.fold(Router::default(), Router::merge)
}
diff --git a/src/events/app.rs b/src/event/app.rs
index 1fa2f70..b5f2ecc 100644
--- a/src/events/app.rs
+++ b/src/event/app.rs
@@ -14,12 +14,9 @@ use super::{
use crate::{
channel,
clock::DateTime,
- repo::{
- channel::Provider as _,
- error::NotFound as _,
- login::Login,
- sequence::{Provider as _, Sequence},
- },
+ event::Sequence,
+ login::Login,
+ repo::{channel::Provider as _, error::NotFound as _, sequence::Provider as _},
};
pub struct Events<'a> {
diff --git a/src/events/broadcaster.rs b/src/event/broadcaster.rs
index 6b664cb..92f631f 100644
--- a/src/events/broadcaster.rs
+++ b/src/event/broadcaster.rs
@@ -1,3 +1,3 @@
-use crate::{broadcast, events::types};
+use crate::{broadcast, event::types};
pub type Broadcaster = broadcast::Broadcaster<types::ChannelEvent>;
diff --git a/src/events/extract.rs b/src/event/extract.rs
index e3021e2..e3021e2 100644
--- a/src/events/extract.rs
+++ b/src/event/extract.rs
diff --git a/src/events/mod.rs b/src/event/mod.rs
index 711ae64..7ad3f9c 100644
--- a/src/events/mod.rs
+++ b/src/event/mod.rs
@@ -3,6 +3,7 @@ pub mod broadcaster;
mod extract;
pub mod repo;
mod routes;
+mod sequence;
pub mod types;
-pub use self::routes::router;
+pub use self::{routes::router, sequence::Sequence};
diff --git a/src/events/repo/message.rs b/src/event/repo/message.rs
index 00c24b1..f051fec 100644
--- a/src/events/repo/message.rs
+++ b/src/event/repo/message.rs
@@ -1,11 +1,11 @@
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
use crate::{
- channel,
+ channel::{self, Channel},
clock::DateTime,
- events::types,
- login, message,
- repo::{channel::Channel, login::Login, message::Message, sequence::Sequence},
+ event::{types, Sequence},
+ login::{self, Login},
+ message::{self, Message},
};
pub trait Provider {
diff --git a/src/events/repo/mod.rs b/src/event/repo/mod.rs
index e216a50..e216a50 100644
--- a/src/events/repo/mod.rs
+++ b/src/event/repo/mod.rs
diff --git a/src/events/routes.rs b/src/event/routes.rs
index d81c7fb..77761ca 100644
--- a/src/events/routes.rs
+++ b/src/event/routes.rs
@@ -14,8 +14,9 @@ use super::{extract::LastEventId, types};
use crate::{
app::App,
error::{Internal, Unauthorized},
- login::{app::ValidateError, extract::Identity},
- repo::sequence::Sequence,
+ event::Sequence,
+ login::app::ValidateError,
+ token::extract::Identity,
};
#[cfg(test)]
diff --git a/src/events/routes/test.rs b/src/event/routes/test.rs
index 11f01b8..9a3b12a 100644
--- a/src/events/routes/test.rs
+++ b/src/event/routes/test.rs
@@ -6,7 +6,7 @@ use futures::{
};
use crate::{
- events::routes,
+ event::routes,
test::fixtures::{self, future::Immediately as _},
};
diff --git a/src/event/sequence.rs b/src/event/sequence.rs
new file mode 100644
index 0000000..9ebddd7
--- /dev/null
+++ b/src/event/sequence.rs
@@ -0,0 +1,24 @@
+use std::fmt;
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Ord,
+ PartialEq,
+ PartialOrd,
+ serde::Deserialize,
+ serde::Serialize,
+ sqlx::Type,
+)]
+#[serde(transparent)]
+#[sqlx(transparent)]
+pub struct Sequence(i64);
+
+impl fmt::Display for Sequence {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let Self(value) = self;
+ value.fmt(f)
+ }
+}
diff --git a/src/events/types.rs b/src/event/types.rs
index 762b6e5..cd7dea6 100644
--- a/src/events/types.rs
+++ b/src/event/types.rs
@@ -1,8 +1,9 @@
use crate::{
- channel,
+ channel::{self, Channel},
clock::DateTime,
- message,
- repo::{channel::Channel, login::Login, message::Message, sequence::Sequence},
+ event::Sequence,
+ login::Login,
+ message::{self, Message},
};
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
diff --git a/src/lib.rs b/src/lib.rs
index 2300071..bbcb314 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -8,12 +8,12 @@ mod channel;
pub mod cli;
mod clock;
mod error;
-mod events;
+mod event;
mod expire;
mod id;
mod login;
mod message;
-mod password;
mod repo;
#[cfg(test)]
mod test;
+mod token;
diff --git a/src/login/app.rs b/src/login/app.rs
index 8ea0a91..60475af 100644
--- a/src/login/app.rs
+++ b/src/login/app.rs
@@ -6,18 +6,15 @@ use futures::{
};
use sqlx::sqlite::SqlitePool;
-use super::{
- broadcaster::Broadcaster, extract::IdentitySecret, repo::auth::Provider as _, token, types,
-};
+use super::{broadcaster::Broadcaster, repo::auth::Provider as _, types, Login};
use crate::{
clock::DateTime,
- password::Password,
+ event::Sequence,
+ login::Password,
repo::{
- error::NotFound as _,
- login::{Login, Provider as _},
- sequence::{Provider as _, Sequence},
- token::Provider as _,
+ error::NotFound as _, login::Provider as _, sequence::Provider as _, token::Provider as _,
},
+ token::{self, Secret},
};
pub struct Logins<'a> {
@@ -43,7 +40,7 @@ impl<'a> Logins<'a> {
name: &str,
password: &Password,
login_at: &DateTime,
- ) -> Result<IdentitySecret, LoginError> {
+ ) -> Result<Secret, LoginError> {
let mut tx = self.db.begin().await?;
let login = if let Some((login, stored_hash)) = tx.auth().for_name(name).await? {
@@ -78,7 +75,7 @@ impl<'a> Logins<'a> {
pub async fn validate(
&self,
- secret: &IdentitySecret,
+ secret: &Secret,
used_at: &DateTime,
) -> Result<(token::Id, Login), ValidateError> {
let mut tx = self.db.begin().await?;
diff --git a/src/login/extract.rs b/src/login/extract.rs
index 39dd9e4..c2d97f2 100644
--- a/src/login/extract.rs
+++ b/src/login/extract.rs
@@ -1,182 +1,15 @@
-use std::fmt;
+use axum::{extract::FromRequestParts, http::request::Parts};
-use axum::{
- extract::{FromRequestParts, State},
- http::request::Parts,
- response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
-};
-use axum_extra::extract::cookie::{Cookie, CookieJar};
-
-use crate::{
- app::App,
- clock::RequestedAt,
- error::{Internal, Unauthorized},
- login::{app::ValidateError, token},
- repo::login::Login,
-};
-
-// The usage pattern here - receive the extractor as an argument, return it in
-// the response - is heavily modelled after CookieJar's own intended usage.
-#[derive(Clone)]
-pub struct IdentityToken {
- cookies: CookieJar,
-}
-
-impl fmt::Debug for IdentityToken {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("IdentityToken")
- .field(
- "identity",
- &self.cookies.get(IDENTITY_COOKIE).map(|_| "********"),
- )
- .finish()
- }
-}
-
-impl IdentityToken {
- // Creates a new, unpopulated identity token store.
- #[cfg(test)]
- pub fn new() -> Self {
- Self {
- cookies: CookieJar::new(),
- }
- }
-
- // Get the identity secret sent in the request, if any. If the identity
- // was not sent, or if it has previously been [clear]ed, then this will
- // return [None]. If the identity has previously been [set], then this
- // will return that secret, regardless of what the request originally
- // included.
- pub fn secret(&self) -> Option<IdentitySecret> {
- self.cookies
- .get(IDENTITY_COOKIE)
- .map(Cookie::value)
- .map(IdentitySecret::from)
- }
-
- // Positively set the identity secret, and ensure that it will be sent
- // back to the client when this extractor is included in a response.
- pub fn set(self, secret: impl Into<IdentitySecret>) -> Self {
- let IdentitySecret(secret) = secret.into();
- let identity_cookie = Cookie::build((IDENTITY_COOKIE, secret))
- .http_only(true)
- .path("/api/")
- .permanent()
- .build();
-
- Self {
- cookies: self.cookies.add(identity_cookie),
- }
- }
-
- // Remove the identity secret and ensure that it will be cleared when this
- // extractor is included in a response.
- pub fn clear(self) -> Self {
- Self {
- cookies: self.cookies.remove(IDENTITY_COOKIE),
- }
- }
-}
-
-const IDENTITY_COOKIE: &str = "identity";
-
-#[async_trait::async_trait]
-impl<S> FromRequestParts<S> for IdentityToken
-where
- S: Send + Sync,
-{
- type Rejection = <CookieJar as FromRequestParts<S>>::Rejection;
-
- async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
- let cookies = CookieJar::from_request_parts(parts, state).await?;
- Ok(Self { cookies })
- }
-}
-
-impl IntoResponseParts for IdentityToken {
- type Error = <CookieJar as IntoResponseParts>::Error;
-
- fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
- let Self { cookies } = self;
- cookies.into_response_parts(res)
- }
-}
-
-#[derive(sqlx::Type)]
-#[sqlx(transparent)]
-pub struct IdentitySecret(String);
-
-impl fmt::Debug for IdentitySecret {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_tuple("IdentityToken").field(&"********").finish()
- }
-}
-
-impl<S> From<S> for IdentitySecret
-where
- S: Into<String>,
-{
- fn from(value: S) -> Self {
- Self(value.into())
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Identity {
- pub token: token::Id,
- pub login: Login,
-}
+use super::Login;
+use crate::{app::App, token::extract::Identity};
#[async_trait::async_trait]
-impl FromRequestParts<App> for Identity {
- type Rejection = LoginError<Internal>;
+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> {
- // After Rust 1.82 (and #[feature(min_exhaustive_patterns)] lands on
- // stable), the following can be replaced:
- //
- // ```
- // let Ok(identity_token) = IdentityToken::from_request_parts(
- // parts,
- // state,
- // ).await;
- // ```
- let identity_token = IdentityToken::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 app = State::<App>::from_request_parts(parts, state).await?;
- match app.logins().validate(&secret, &used_at).await {
- Ok((token, login)) => Ok(Identity { token, login }),
- Err(ValidateError::InvalidToken) => Err(LoginError::Unauthorized),
- Err(other) => Err(other.into()),
- }
- }
-}
-
-pub enum LoginError<E> {
- Failure(E),
- Unauthorized,
-}
-
-impl<E> IntoResponse for LoginError<E>
-where
- E: IntoResponse,
-{
- fn into_response(self) -> Response {
- match self {
- Self::Unauthorized => Unauthorized.into_response(),
- Self::Failure(e) => e.into_response(),
- }
- }
-}
+ let identity = Identity::from_request_parts(parts, state).await?;
-impl<E> From<E> for LoginError<Internal>
-where
- E: Into<Internal>,
-{
- fn from(err: E) -> Self {
- Self::Failure(err.into())
+ Ok(identity.login)
}
}
diff --git a/src/login/mod.rs b/src/login/mod.rs
index 0430f4b..91c1821 100644
--- a/src/login/mod.rs
+++ b/src/login/mod.rs
@@ -2,10 +2,22 @@ pub mod app;
pub mod broadcaster;
pub mod extract;
mod id;
+pub mod password;
mod repo;
mod routes;
-pub mod token;
pub mod types;
-pub use self::id::Id;
-pub use self::routes::router;
+pub use self::{id::Id, password::Password, routes::router};
+
+// This also implements FromRequestParts (see `./extract.rs`). As a result, it
+// can be used as an extractor for endpoints that want to require login, or for
+// endpoints that need to behave differently depending on whether the client is
+// or is not logged in.
+#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
+pub struct Login {
+ pub id: Id,
+ pub name: String,
+ // The omission of the hashed password is deliberate, to minimize the
+ // chance that it ends up tangled up in debug output or in some other chunk
+ // of logic elsewhere.
+}
diff --git a/src/password.rs b/src/login/password.rs
index da3930f..da3930f 100644
--- a/src/password.rs
+++ b/src/login/password.rs
diff --git a/src/login/repo/auth.rs b/src/login/repo/auth.rs
index 9816c5c..b299697 100644
--- a/src/login/repo/auth.rs
+++ b/src/login/repo/auth.rs
@@ -1,6 +1,6 @@
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
-use crate::{login, password::StoredHash, repo::login::Login};
+use crate::login::{self, password::StoredHash, Login};
pub trait Provider {
fn auth(&mut self) -> Auth;
diff --git a/src/login/routes.rs b/src/login/routes.rs
index ef75871..b571bd5 100644
--- a/src/login/routes.rs
+++ b/src/login/routes.rs
@@ -10,11 +10,11 @@ use crate::{
app::App,
clock::RequestedAt,
error::{Internal, Unauthorized},
- password::Password,
- repo::login::Login,
+ login::{Login, Password},
};
-use super::{app, extract::IdentityToken};
+use super::app;
+use crate::token::extract::IdentityToken;
#[cfg(test)]
mod test;
diff --git a/src/login/token/mod.rs b/src/login/token/mod.rs
deleted file mode 100644
index d563a88..0000000
--- a/src/login/token/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-mod id;
-
-pub use self::id::Id;
diff --git a/src/login/types.rs b/src/login/types.rs
index a210977..d53d436 100644
--- a/src/login/types.rs
+++ b/src/login/types.rs
@@ -1,4 +1,4 @@
-use crate::login::token;
+use crate::token;
#[derive(Clone, Debug)]
pub struct TokenRevoked {
diff --git a/src/message/mod.rs b/src/message/mod.rs
index d563a88..9a9bf14 100644
--- a/src/message/mod.rs
+++ b/src/message/mod.rs
@@ -1,3 +1,9 @@
mod id;
pub use self::id::Id;
+
+#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
+pub struct Message {
+ pub id: Id,
+ pub body: String,
+}
diff --git a/src/repo/channel.rs b/src/repo/channel.rs
index 9f1d930..18cd81f 100644
--- a/src/repo/channel.rs
+++ b/src/repo/channel.rs
@@ -1,10 +1,9 @@
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
-use super::sequence::Sequence;
use crate::{
- channel::Id,
+ channel::{Channel, Id},
clock::DateTime,
- events::types::{self},
+ event::{types, Sequence},
};
pub trait Provider {
@@ -19,16 +18,6 @@ impl<'c> Provider for Transaction<'c, Sqlite> {
pub struct Channels<'t>(&'t mut SqliteConnection);
-#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
-pub struct Channel {
- pub id: Id,
- pub name: String,
- #[serde(skip)]
- pub created_at: DateTime,
- #[serde(skip)]
- pub created_sequence: Sequence,
-}
-
impl<'c> Channels<'c> {
pub async fn create(
&mut self,
diff --git a/src/repo/login/store.rs b/src/repo/login.rs
index 47d1a7c..d1a02c4 100644
--- a/src/repo/login/store.rs
+++ b/src/repo/login.rs
@@ -1,6 +1,6 @@
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
-use crate::{login::Id, password::StoredHash};
+use crate::login::{password::StoredHash, Id, Login};
pub trait Provider {
fn logins(&mut self) -> Logins;
@@ -14,19 +14,6 @@ impl<'c> Provider for Transaction<'c, Sqlite> {
pub struct Logins<'t>(&'t mut SqliteConnection);
-// This also implements FromRequestParts (see `./extract.rs`). As a result, it
-// can be used as an extractor for endpoints that want to require login, or for
-// endpoints that need to behave differently depending on whether the client is
-// or is not logged in.
-#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
-pub struct Login {
- pub id: Id,
- pub name: String,
- // The omission of the hashed password is deliberate, to minimize the
- // chance that it ends up tangled up in debug output or in some other chunk
- // of logic elsewhere.
-}
-
impl<'c> Logins<'c> {
pub async fn create(
&mut self,
diff --git a/src/repo/login/extract.rs b/src/repo/login/extract.rs
deleted file mode 100644
index ab61106..0000000
--- a/src/repo/login/extract.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use axum::{extract::FromRequestParts, http::request::Parts};
-
-use super::Login;
-use crate::{app::App, login::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/repo/login/mod.rs b/src/repo/login/mod.rs
deleted file mode 100644
index 4ff7a96..0000000
--- a/src/repo/login/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-mod extract;
-mod store;
-
-pub use self::store::{Login, Provider};
diff --git a/src/repo/message.rs b/src/repo/message.rs
deleted file mode 100644
index acde3ea..0000000
--- a/src/repo/message.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-use crate::message::Id;
-
-#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
-pub struct Message {
- pub id: Id,
- pub body: String,
-}
diff --git a/src/repo/mod.rs b/src/repo/mod.rs
index 8f271f4..69ad82c 100644
--- a/src/repo/mod.rs
+++ b/src/repo/mod.rs
@@ -1,7 +1,6 @@
pub mod channel;
pub mod error;
pub mod login;
-pub mod message;
pub mod pool;
pub mod sequence;
pub mod token;
diff --git a/src/repo/sequence.rs b/src/repo/sequence.rs
index c47b41c..c985869 100644
--- a/src/repo/sequence.rs
+++ b/src/repo/sequence.rs
@@ -1,7 +1,7 @@
-use std::fmt;
-
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
+use crate::event::Sequence;
+
pub trait Provider {
fn sequence(&mut self) -> Sequences;
}
@@ -42,26 +42,3 @@ impl<'c> Sequences<'c> {
Ok(next)
}
}
-
-#[derive(
- Clone,
- Copy,
- Debug,
- Eq,
- Ord,
- PartialEq,
- PartialOrd,
- serde::Deserialize,
- serde::Serialize,
- sqlx::Type,
-)]
-#[serde(transparent)]
-#[sqlx(transparent)]
-pub struct Sequence(i64);
-
-impl fmt::Display for Sequence {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let Self(value) = self;
- value.fmt(f)
- }
-}
diff --git a/src/repo/token.rs b/src/repo/token.rs
index 79e5c54..5f64dac 100644
--- a/src/repo/token.rs
+++ b/src/repo/token.rs
@@ -1,10 +1,10 @@
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
use uuid::Uuid;
-use super::login::Login;
use crate::{
clock::DateTime,
- login::{self, extract::IdentitySecret, token::Id},
+ login::{self, Login},
+ token::{Id, Secret},
};
pub trait Provider {
@@ -26,7 +26,7 @@ impl<'c> Tokens<'c> {
&mut self,
login: &Login,
issued_at: &DateTime,
- ) -> Result<IdentitySecret, sqlx::Error> {
+ ) -> Result<Secret, sqlx::Error> {
let id = Id::generate();
let secret = Uuid::new_v4().to_string();
@@ -35,7 +35,7 @@ impl<'c> Tokens<'c> {
insert
into token (id, secret, login, issued_at, last_used_at)
values ($1, $2, $3, $4, $4)
- returning secret as "secret!: IdentitySecret"
+ returning secret as "secret!: Secret"
"#,
id,
secret,
@@ -103,7 +103,7 @@ impl<'c> Tokens<'c> {
// timestamp will be set to `used_at`.
pub async fn validate(
&mut self,
- secret: &IdentitySecret,
+ secret: &Secret,
used_at: &DateTime,
) -> Result<(Id, Login), sqlx::Error> {
// I would use `update … returning` to do this in one query, but
diff --git a/src/test/fixtures/channel.rs b/src/test/fixtures/channel.rs
index 8744470..b678717 100644
--- a/src/test/fixtures/channel.rs
+++ b/src/test/fixtures/channel.rs
@@ -4,7 +4,7 @@ use faker_rand::{
};
use rand;
-use crate::{app::App, clock::RequestedAt, repo::channel::Channel};
+use crate::{app::App, channel::Channel, clock::RequestedAt};
pub async fn create(app: &App, created_at: &RequestedAt) -> Channel {
let name = propose();
diff --git a/src/test/fixtures/filter.rs b/src/test/fixtures/filter.rs
index c31fa58..d1939a5 100644
--- a/src/test/fixtures/filter.rs
+++ b/src/test/fixtures/filter.rs
@@ -1,6 +1,6 @@
use futures::future;
-use crate::events::types;
+use crate::event::types;
pub fn messages() -> impl FnMut(&types::ChannelEvent) -> future::Ready<bool> {
|event| future::ready(matches!(event.data, types::ChannelEventData::Message(_)))
diff --git a/src/test/fixtures/identity.rs b/src/test/fixtures/identity.rs
index 633fb8a..9e8e403 100644
--- a/src/test/fixtures/identity.rs
+++ b/src/test/fixtures/identity.rs
@@ -3,8 +3,11 @@ use uuid::Uuid;
use crate::{
app::App,
clock::RequestedAt,
- login::extract::{Identity, IdentitySecret, IdentityToken},
- password::Password,
+ login::Password,
+ token::{
+ extract::{Identity, IdentityToken},
+ Secret,
+ },
};
pub fn not_logged_in() -> IdentityToken {
@@ -38,7 +41,7 @@ pub async fn identity(app: &App, login: &(String, Password), issued_at: &Request
from_token(app, &secret, issued_at).await
}
-pub fn secret(identity: &IdentityToken) -> IdentitySecret {
+pub fn secret(identity: &IdentityToken) -> Secret {
identity.secret().expect("identity contained a secret")
}
diff --git a/src/test/fixtures/login.rs b/src/test/fixtures/login.rs
index d6a321b..00c2789 100644
--- a/src/test/fixtures/login.rs
+++ b/src/test/fixtures/login.rs
@@ -3,8 +3,7 @@ use uuid::Uuid;
use crate::{
app::App,
- password::Password,
- repo::login::{self, Login},
+ login::{self, Login, Password},
};
pub async fn create_with_password(app: &App) -> (String, Password) {
diff --git a/src/test/fixtures/message.rs b/src/test/fixtures/message.rs
index bfca8cd..fd50887 100644
--- a/src/test/fixtures/message.rs
+++ b/src/test/fixtures/message.rs
@@ -1,11 +1,6 @@
use faker_rand::lorem::Paragraphs;
-use crate::{
- app::App,
- clock::RequestedAt,
- events::types,
- repo::{channel::Channel, login::Login},
-};
+use crate::{app::App, channel::Channel, clock::RequestedAt, event::types, login::Login};
pub async fn send(
app: &App,
diff --git a/src/token/extract/identity.rs b/src/token/extract/identity.rs
new file mode 100644
index 0000000..42c7c60
--- /dev/null
+++ b/src/token/extract/identity.rs
@@ -0,0 +1,75 @@
+use axum::{
+ extract::{FromRequestParts, State},
+ http::request::Parts,
+ response::{IntoResponse, Response},
+};
+
+use super::IdentityToken;
+
+use crate::{
+ app::App,
+ clock::RequestedAt,
+ error::{Internal, Unauthorized},
+ login::{app::ValidateError, Login},
+ token,
+};
+
+#[derive(Clone, Debug)]
+pub struct Identity {
+ pub token: token::Id,
+ pub login: Login,
+}
+
+#[async_trait::async_trait]
+impl FromRequestParts<App> for Identity {
+ type Rejection = LoginError<Internal>;
+
+ async fn from_request_parts(parts: &mut Parts, state: &App) -> Result<Self, Self::Rejection> {
+ // After Rust 1.82 (and #[feature(min_exhaustive_patterns)] lands on
+ // stable), the following can be replaced:
+ //
+ // ```
+ // let Ok(identity_token) = IdentityToken::from_request_parts(
+ // parts,
+ // state,
+ // ).await;
+ // ```
+ let identity_token = IdentityToken::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 app = State::<App>::from_request_parts(parts, state).await?;
+ match app.logins().validate(&secret, &used_at).await {
+ Ok((token, login)) => Ok(Identity { token, login }),
+ Err(ValidateError::InvalidToken) => Err(LoginError::Unauthorized),
+ Err(other) => Err(other.into()),
+ }
+ }
+}
+
+pub enum LoginError<E> {
+ Failure(E),
+ Unauthorized,
+}
+
+impl<E> IntoResponse for LoginError<E>
+where
+ E: IntoResponse,
+{
+ fn into_response(self) -> Response {
+ match self {
+ Self::Unauthorized => Unauthorized.into_response(),
+ Self::Failure(e) => e.into_response(),
+ }
+ }
+}
+
+impl<E> From<E> for LoginError<Internal>
+where
+ E: Into<Internal>,
+{
+ fn from(err: E) -> Self {
+ Self::Failure(err.into())
+ }
+}
diff --git a/src/token/extract/identity_token.rs b/src/token/extract/identity_token.rs
new file mode 100644
index 0000000..0a47a43
--- /dev/null
+++ b/src/token/extract/identity_token.rs
@@ -0,0 +1,94 @@
+use std::fmt;
+
+use axum::{
+ extract::FromRequestParts,
+ http::request::Parts,
+ response::{IntoResponseParts, ResponseParts},
+};
+use axum_extra::extract::cookie::{Cookie, CookieJar};
+
+use crate::token::Secret;
+
+// The usage pattern here - receive the extractor as an argument, return it in
+// the response - is heavily modelled after CookieJar's own intended usage.
+#[derive(Clone)]
+pub struct IdentityToken {
+ cookies: CookieJar,
+}
+
+impl fmt::Debug for IdentityToken {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("IdentityToken")
+ .field("identity", &self.secret())
+ .finish()
+ }
+}
+
+impl IdentityToken {
+ // Creates a new, unpopulated identity token store.
+ #[cfg(test)]
+ pub fn new() -> Self {
+ Self {
+ cookies: CookieJar::new(),
+ }
+ }
+
+ // Get the identity secret sent in the request, if any. If the identity
+ // was not sent, or if it has previously been [clear]ed, then this will
+ // return [None]. If the identity has previously been [set], then this
+ // will return that secret, regardless of what the request originally
+ // included.
+ pub fn secret(&self) -> Option<Secret> {
+ self.cookies
+ .get(IDENTITY_COOKIE)
+ .map(Cookie::value)
+ .map(Secret::from)
+ }
+
+ // Positively set the identity secret, and ensure that it will be sent
+ // back to the client when this extractor is included in a response.
+ pub fn set(self, secret: impl Into<Secret>) -> Self {
+ let secret = secret.into().reveal();
+ let identity_cookie = Cookie::build((IDENTITY_COOKIE, secret))
+ .http_only(true)
+ .path("/api/")
+ .permanent()
+ .build();
+
+ Self {
+ cookies: self.cookies.add(identity_cookie),
+ }
+ }
+
+ // Remove the identity secret and ensure that it will be cleared when this
+ // extractor is included in a response.
+ pub fn clear(self) -> Self {
+ Self {
+ cookies: self.cookies.remove(IDENTITY_COOKIE),
+ }
+ }
+}
+
+const IDENTITY_COOKIE: &str = "identity";
+
+#[async_trait::async_trait]
+impl<S> FromRequestParts<S> for IdentityToken
+where
+ S: Send + Sync,
+{
+ type Rejection = <CookieJar as FromRequestParts<S>>::Rejection;
+
+ async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
+ let cookies = CookieJar::from_request_parts(parts, state).await?;
+ Ok(Self { cookies })
+ }
+}
+
+impl IntoResponseParts for IdentityToken {
+ type Error = <CookieJar as IntoResponseParts>::Error;
+
+ fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
+ let Self { cookies } = self;
+ cookies.into_response_parts(res)
+ }
+}
diff --git a/src/token/extract/mod.rs b/src/token/extract/mod.rs
new file mode 100644
index 0000000..b4800ae
--- /dev/null
+++ b/src/token/extract/mod.rs
@@ -0,0 +1,4 @@
+mod identity;
+mod identity_token;
+
+pub use self::{identity::Identity, identity_token::IdentityToken};
diff --git a/src/login/token/id.rs b/src/token/id.rs
index 9ef063c..9ef063c 100644
--- a/src/login/token/id.rs
+++ b/src/token/id.rs
diff --git a/src/token/mod.rs b/src/token/mod.rs
new file mode 100644
index 0000000..c98b8c2
--- /dev/null
+++ b/src/token/mod.rs
@@ -0,0 +1,5 @@
+pub mod extract;
+mod id;
+mod secret;
+
+pub use self::{id::Id, secret::Secret};
diff --git a/src/token/secret.rs b/src/token/secret.rs
new file mode 100644
index 0000000..28c93bb
--- /dev/null
+++ b/src/token/secret.rs
@@ -0,0 +1,27 @@
+use std::fmt;
+
+#[derive(sqlx::Type)]
+#[sqlx(transparent)]
+pub struct Secret(String);
+
+impl Secret {
+ pub fn reveal(self) -> String {
+ let Self(secret) = self;
+ secret
+ }
+}
+
+impl fmt::Debug for Secret {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_tuple("IdentityToken").field(&"********").finish()
+ }
+}
+
+impl<S> From<S> for Secret
+where
+ S: Into<String>,
+{
+ fn from(value: S) -> Self {
+ Self(value.into())
+ }
+}