summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/channel/app.rs40
-rw-r--r--src/channel/repo/broadcast.rs112
-rw-r--r--src/channel/repo/messages.rs136
-rw-r--r--src/channel/repo/mod.rs3
-rw-r--r--src/channel/routes.rs10
-rw-r--r--src/events.rs13
-rw-r--r--src/index/app.rs4
-rw-r--r--src/index/routes.rs7
-rw-r--r--src/index/templates.rs2
-rw-r--r--src/lib.rs2
-rw-r--r--src/login/app.rs46
-rw-r--r--src/login/extract.rs (renamed from src/login/extract/identity_token.rs)2
-rw-r--r--src/login/extract/mod.rs4
-rw-r--r--src/login/mod.rs4
-rw-r--r--src/login/repo/auth.rs53
-rw-r--r--src/login/repo/mod.rs3
-rw-r--r--src/password.rs31
-rw-r--r--src/repo/channel.rs (renamed from src/channel/repo/channels.rs)0
-rw-r--r--src/repo/login/extract.rs (renamed from src/login/extract/login.rs)8
-rw-r--r--src/repo/login/mod.rs4
-rw-r--r--src/repo/login/store.rs (renamed from src/login/repo/logins.rs)42
-rw-r--r--src/repo/message.rs27
-rw-r--r--src/repo/mod.rs4
-rw-r--r--src/repo/token.rs (renamed from src/login/repo/tokens.rs)6
24 files changed, 299 insertions, 264 deletions
diff --git a/src/channel/app.rs b/src/channel/app.rs
index 36fa552..b0b63b3 100644
--- a/src/channel/app.rs
+++ b/src/channel/app.rs
@@ -10,11 +10,15 @@ use sqlx::sqlite::SqlitePool;
use tokio::sync::broadcast::{channel, Sender};
use tokio_stream::wrappers::BroadcastStream;
-use super::repo::{
- channels::{Channel, Id as ChannelId, Provider as _},
- messages::{BroadcastMessage, Provider as _},
+use super::repo::broadcast::{self, Provider as _};
+use crate::{
+ clock::DateTime,
+ error::BoxedError,
+ repo::{
+ channel::{self, Channel, Provider as _},
+ login::Login,
+ },
};
-use crate::{clock::DateTime, error::BoxedError, login::repo::logins::Login};
pub struct Channels<'a> {
db: &'a SqlitePool,
@@ -46,13 +50,13 @@ impl<'a> Channels<'a> {
pub async fn send(
&self,
login: &Login,
- channel: &ChannelId,
+ channel: &channel::Id,
body: &str,
sent_at: &DateTime,
) -> Result<(), BoxedError> {
let mut tx = self.db.begin().await?;
let message = tx
- .messages()
+ .broadcast()
.create(&login.id, channel, body, sent_at)
.await?;
tx.commit().await?;
@@ -63,13 +67,13 @@ impl<'a> Channels<'a> {
pub async fn events(
&self,
- channel: &ChannelId,
+ channel: &channel::Id,
resume_at: Option<&DateTime>,
- ) -> Result<impl Stream<Item = Result<BroadcastMessage, BoxedError>> + 'static, BoxedError>
+ ) -> Result<impl Stream<Item = Result<broadcast::Message, BoxedError>> + 'static, BoxedError>
{
fn skip_stale<E>(
resume_at: Option<&DateTime>,
- ) -> impl for<'m> FnMut(&'m BroadcastMessage) -> future::Ready<Result<bool, E>> {
+ ) -> impl for<'m> FnMut(&'m broadcast::Message) -> future::Ready<Result<bool, E>> {
let resume_at = resume_at.cloned();
move |msg| {
future::ready(Ok(match resume_at {
@@ -86,7 +90,7 @@ impl<'a> Channels<'a> {
.try_skip_while(skip_stale(resume_at));
let mut tx = self.db.begin().await?;
- let stored_messages = tx.messages().for_replay(channel, resume_at).await?;
+ let stored_messages = tx.broadcast().replay(channel, resume_at).await?;
tx.commit().await?;
let stored_messages = stream::iter(stored_messages).map(Ok);
@@ -101,7 +105,7 @@ pub struct Broadcaster {
// The use of std::sync::Mutex, and not tokio::sync::Mutex, follows Tokio's
// own advice: <https://tokio.rs/tokio/tutorial/shared-state>. Methods that
// lock it must be sync.
- senders: Arc<Mutex<HashMap<ChannelId, Sender<BroadcastMessage>>>>,
+ senders: Arc<Mutex<HashMap<channel::Id, Sender<broadcast::Message>>>>,
}
impl Broadcaster {
@@ -115,7 +119,7 @@ impl Broadcaster {
Ok(broadcaster)
}
- fn new<'i>(channels: impl IntoIterator<Item = &'i ChannelId>) -> Self {
+ fn new<'i>(channels: impl IntoIterator<Item = &'i channel::Id>) -> Self {
let senders: HashMap<_, _> = channels
.into_iter()
.cloned()
@@ -128,7 +132,7 @@ impl Broadcaster {
}
// panic: if ``channel`` is already registered.
- pub fn register_channel(&self, channel: &ChannelId) {
+ pub fn register_channel(&self, channel: &channel::Id) {
match self.senders().entry(channel.clone()) {
// This ever happening indicates a serious logic error.
Entry::Occupied(_) => panic!("duplicate channel registration for channel {channel}"),
@@ -140,7 +144,7 @@ impl Broadcaster {
// panic: if ``channel`` has not been previously registered, and was not
// part of the initial set of channels.
- pub fn broadcast(&self, channel: &ChannelId, message: BroadcastMessage) {
+ pub fn broadcast(&self, channel: &channel::Id, message: broadcast::Message) {
let tx = self.sender(channel);
// Per the Tokio docs, the returned error is only used to indicate that
@@ -152,7 +156,7 @@ impl Broadcaster {
// panic: if ``channel`` has not been previously registered, and was not
// part of the initial set of channels.
- pub fn listen(&self, channel: &ChannelId) -> BroadcastStream<BroadcastMessage> {
+ pub fn listen(&self, channel: &channel::Id) -> BroadcastStream<broadcast::Message> {
let rx = self.sender(channel).subscribe();
BroadcastStream::from(rx)
@@ -160,15 +164,15 @@ impl Broadcaster {
// panic: if ``channel`` has not been previously registered, and was not
// part of the initial set of channels.
- fn sender(&self, channel: &ChannelId) -> Sender<BroadcastMessage> {
+ fn sender(&self, channel: &channel::Id) -> Sender<broadcast::Message> {
self.senders()[channel].clone()
}
- fn senders(&self) -> MutexGuard<HashMap<ChannelId, Sender<BroadcastMessage>>> {
+ fn senders(&self) -> MutexGuard<HashMap<channel::Id, Sender<broadcast::Message>>> {
self.senders.lock().unwrap() // propagate panics when mutex is poisoned
}
- fn make_sender() -> Sender<BroadcastMessage> {
+ fn make_sender() -> Sender<broadcast::Message> {
// Queue depth of 16 chosen entirely arbitrarily. Don't read too much
// into it.
let (tx, _) = channel(16);
diff --git a/src/channel/repo/broadcast.rs b/src/channel/repo/broadcast.rs
new file mode 100644
index 0000000..3ca7396
--- /dev/null
+++ b/src/channel/repo/broadcast.rs
@@ -0,0 +1,112 @@
+use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
+
+use crate::{
+ clock::DateTime,
+ repo::{
+ channel,
+ login::{self, Login, Logins},
+ message,
+ },
+};
+
+pub trait Provider {
+ fn broadcast(&mut self) -> Broadcast;
+}
+
+impl<'c> Provider for Transaction<'c, Sqlite> {
+ fn broadcast(&mut self) -> Broadcast {
+ Broadcast(self)
+ }
+}
+
+pub struct Broadcast<'t>(&'t mut SqliteConnection);
+
+#[derive(Clone, Debug, serde::Serialize)]
+pub struct Message {
+ pub id: message::Id,
+ pub sender: Login,
+ pub body: String,
+ pub sent_at: DateTime,
+}
+
+impl<'c> Broadcast<'c> {
+ pub async fn create(
+ &mut self,
+ sender: &login::Id,
+ channel: &channel::Id,
+ body: &str,
+ sent_at: &DateTime,
+ ) -> Result<Message, sqlx::Error> {
+ let id = message::Id::generate();
+
+ let sender = Logins::from(&mut *self.0).by_id(sender).await?;
+
+ let message = sqlx::query!(
+ r#"
+ insert into message
+ (id, sender, channel, body, sent_at)
+ values ($1, $2, $3, $4, $5)
+ returning
+ id as "id: message::Id",
+ sender as "sender: login::Id",
+ body,
+ sent_at as "sent_at: DateTime"
+ "#,
+ id,
+ sender.id,
+ channel,
+ body,
+ sent_at,
+ )
+ .map(|row| {
+ debug_assert!(row.sender == sender.id);
+ Message {
+ id: row.id,
+ sender: sender.clone(),
+ body: row.body,
+ sent_at: row.sent_at,
+ }
+ })
+ .fetch_one(&mut *self.0)
+ .await?;
+
+ Ok(message)
+ }
+
+ pub async fn replay(
+ &mut self,
+ channel: &channel::Id,
+ resume_at: Option<&DateTime>,
+ ) -> Result<Vec<Message>, sqlx::Error> {
+ let messages = sqlx::query!(
+ r#"
+ select
+ message.id as "id: message::Id",
+ login.id as "sender_id: login::Id",
+ login.name as sender_name,
+ message.body,
+ message.sent_at as "sent_at: DateTime"
+ from message
+ join login on message.sender = login.id
+ where channel = $1
+ and coalesce(sent_at > $2, true)
+ order by sent_at asc
+ "#,
+ channel,
+ resume_at,
+ )
+ .map(|row| Message {
+ id: row.id,
+ sender: Login {
+ id: row.sender_id,
+ name: row.sender_name,
+ },
+ body: row.body,
+ sent_at: row.sent_at,
+ })
+ .fetch_all(&mut *self.0)
+ .await?;
+
+ Ok(messages)
+ }
+}
diff --git a/src/channel/repo/messages.rs b/src/channel/repo/messages.rs
deleted file mode 100644
index a30e6da..0000000
--- a/src/channel/repo/messages.rs
+++ /dev/null
@@ -1,136 +0,0 @@
-use std::fmt;
-
-use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
-
-use super::channels::Id as ChannelId;
-use crate::{
- clock::DateTime,
- id::Id as BaseId,
- login::repo::logins::{Id as LoginId, Login, Logins},
-};
-
-pub trait Provider {
- fn messages(&mut self) -> Messages;
-}
-
-impl<'c> Provider for Transaction<'c, Sqlite> {
- fn messages(&mut self) -> Messages {
- Messages(self)
- }
-}
-
-pub struct Messages<'t>(&'t mut SqliteConnection);
-
-#[derive(Clone, Debug, serde::Serialize)]
-pub struct BroadcastMessage {
- pub id: Id,
- pub sender: Login,
- pub body: String,
- pub sent_at: DateTime,
-}
-
-impl<'c> Messages<'c> {
- pub async fn create(
- &mut self,
- sender: &LoginId,
- channel: &ChannelId,
- body: &str,
- sent_at: &DateTime,
- ) -> Result<BroadcastMessage, sqlx::Error> {
- let id = Id::generate();
-
- let sender = Logins::from(&mut *self.0).by_id(sender).await?;
-
- let message = sqlx::query!(
- r#"
- insert into message
- (id, sender, channel, body, sent_at)
- values ($1, $2, $3, $4, $5)
- returning
- id as "id: Id",
- sender as "sender: LoginId",
- body,
- sent_at as "sent_at: DateTime"
- "#,
- id,
- sender.id,
- channel,
- body,
- sent_at,
- )
- .map(|row| {
- debug_assert!(row.sender == sender.id);
- BroadcastMessage {
- id: row.id,
- sender: sender.clone(),
- body: row.body,
- sent_at: row.sent_at,
- }
- })
- .fetch_one(&mut *self.0)
- .await?;
-
- Ok(message)
- }
-
- pub async fn for_replay(
- &mut self,
- channel: &ChannelId,
- resume_at: Option<&DateTime>,
- ) -> Result<Vec<BroadcastMessage>, sqlx::Error> {
- let messages = sqlx::query!(
- r#"
- select
- message.id as "id: Id",
- login.id as "sender_id: LoginId",
- login.name as sender_name,
- message.body,
- message.sent_at as "sent_at: DateTime"
- from message
- join login on message.sender = login.id
- where channel = $1
- and coalesce(sent_at > $2, true)
- order by sent_at asc
- "#,
- channel,
- resume_at,
- )
- .map(|row| BroadcastMessage {
- id: row.id,
- sender: Login {
- id: row.sender_id,
- name: row.sender_name,
- },
- body: row.body,
- sent_at: row.sent_at,
- })
- .fetch_all(&mut *self.0)
- .await?;
-
- Ok(messages)
- }
-}
-
-/// Stable identifier for a [Message]. Prefixed with `M`.
-#[derive(Clone, Debug, Eq, Hash, PartialEq, sqlx::Type, serde::Deserialize, serde::Serialize)]
-#[sqlx(transparent)]
-#[serde(transparent)]
-pub struct Id(BaseId);
-
-impl From<BaseId> for Id {
- fn from(id: BaseId) -> Self {
- Self(id)
- }
-}
-
-impl Id {
- pub fn generate() -> Self {
- BaseId::generate("M")
- }
-}
-
-impl fmt::Display for Id {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
- }
-}
diff --git a/src/channel/repo/mod.rs b/src/channel/repo/mod.rs
index 345897d..2ed3062 100644
--- a/src/channel/repo/mod.rs
+++ b/src/channel/repo/mod.rs
@@ -1,2 +1 @@
-pub mod channels;
-pub mod messages;
+pub mod broadcast;
diff --git a/src/channel/routes.rs b/src/channel/routes.rs
index 3c2353b..1379153 100644
--- a/src/channel/routes.rs
+++ b/src/channel/routes.rs
@@ -5,8 +5,12 @@ use axum::{
Router,
};
-use super::repo::channels::Id as ChannelId;
-use crate::{app::App, clock::RequestedAt, error::InternalError, login::repo::logins::Login};
+use crate::{
+ app::App,
+ clock::RequestedAt,
+ error::InternalError,
+ repo::{channel, login::Login},
+};
pub fn router() -> Router<App> {
Router::new()
@@ -35,7 +39,7 @@ struct SendRequest {
}
async fn on_send(
- Path(channel): Path<ChannelId>,
+ Path(channel): Path<channel::Id>,
RequestedAt(sent_at): RequestedAt,
State(app): State<App>,
login: Login,
diff --git a/src/events.rs b/src/events.rs
index 51f06b4..38d53fc 100644
--- a/src/events.rs
+++ b/src/events.rs
@@ -17,10 +17,9 @@ use futures::{
use crate::{
app::App,
- channel::repo::{channels::Id as ChannelId, messages::BroadcastMessage},
- error::BoxedError,
- error::InternalError,
- login::repo::logins::Login,
+ channel::repo::broadcast,
+ error::{BoxedError, InternalError},
+ repo::{channel, login::Login},
};
pub fn router() -> Router<App> {
@@ -30,7 +29,7 @@ pub fn router() -> Router<App> {
#[derive(serde::Deserialize)]
struct EventsQuery {
#[serde(default, rename = "channel")]
- channels: Vec<ChannelId>,
+ channels: Vec<channel::Id>,
}
async fn on_events(
@@ -71,7 +70,7 @@ async fn on_events(
Ok(sse)
}
-fn to_sse_event(event: ChannelEvent<BroadcastMessage>) -> Result<sse::Event, BoxedError> {
+fn to_sse_event(event: ChannelEvent<broadcast::Message>) -> Result<sse::Event, BoxedError> {
let data = serde_json::to_string(&event)?;
let event = sse::Event::default()
.id(event
@@ -85,7 +84,7 @@ fn to_sse_event(event: ChannelEvent<BroadcastMessage>) -> Result<sse::Event, Box
#[derive(serde::Serialize)]
struct ChannelEvent<M> {
- channel: ChannelId,
+ channel: channel::Id,
#[serde(flatten)]
message: M,
}
diff --git a/src/index/app.rs b/src/index/app.rs
index fabf35c..41b12fa 100644
--- a/src/index/app.rs
+++ b/src/index/app.rs
@@ -1,8 +1,8 @@
use sqlx::sqlite::SqlitePool;
use crate::{
- channel::repo::channels::{Channel, Id as ChannelId, Provider as _},
error::BoxedError,
+ repo::channel::{self, Channel, Provider as _},
};
pub struct Index<'a> {
@@ -14,7 +14,7 @@ impl<'a> Index<'a> {
Self { db }
}
- pub async fn channel(&self, channel: ChannelId) -> Result<Channel, BoxedError> {
+ pub async fn channel(&self, channel: channel::Id) -> Result<Channel, BoxedError> {
let mut tx = self.db.begin().await?;
let channel = tx.channels().by_id(channel).await?;
tx.commit().await?;
diff --git a/src/index/routes.rs b/src/index/routes.rs
index 2d77e5b..ef46cc1 100644
--- a/src/index/routes.rs
+++ b/src/index/routes.rs
@@ -9,8 +9,9 @@ use maud::Markup;
use super::templates;
use crate::{
- app::App, channel::repo::channels::Id as ChannelId, error::InternalError,
- login::repo::logins::Login,
+ app::App,
+ error::InternalError,
+ repo::{channel, login::Login},
};
async fn index(State(app): State<App>, login: Option<Login>) -> Result<Markup, InternalError> {
@@ -47,7 +48,7 @@ async fn js(Path(path): Path<String>) -> impl IntoResponse {
async fn channel(
State(app): State<App>,
_: Login,
- Path(channel): Path<ChannelId>,
+ Path(channel): Path<channel::Id>,
) -> Result<Markup, InternalError> {
let channel = app.index().channel(channel).await?;
Ok(templates::channel(&channel))
diff --git a/src/index/templates.rs b/src/index/templates.rs
index a69c19a..d56972c 100644
--- a/src/index/templates.rs
+++ b/src/index/templates.rs
@@ -1,6 +1,6 @@
use maud::{html, Markup, DOCTYPE};
-use crate::{channel::repo::channels::Channel, login::repo::logins::Login};
+use crate::repo::{channel::Channel, login::Login};
pub fn authenticated<'c>(login: Login, channels: impl IntoIterator<Item = &'c Channel>) -> Markup {
html! {
diff --git a/src/lib.rs b/src/lib.rs
index 3813c28..f71ef95 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,3 +7,5 @@ mod events;
mod id;
mod index;
mod login;
+mod password;
+mod repo;
diff --git a/src/login/app.rs b/src/login/app.rs
index cd65f35..c82da1a 100644
--- a/src/login/app.rs
+++ b/src/login/app.rs
@@ -1,13 +1,15 @@
-use argon2::Argon2;
-use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
-use rand_core::OsRng;
use sqlx::sqlite::SqlitePool;
-use super::repo::{
- logins::{Login, Provider as _},
- tokens::Provider as _,
+use super::repo::auth::Provider as _;
+use crate::{
+ clock::DateTime,
+ error::BoxedError,
+ password::StoredHash,
+ repo::{
+ login::{Login, Provider as _},
+ token::Provider as _,
+ },
};
-use crate::{clock::DateTime, error::BoxedError};
pub struct Logins<'a> {
db: &'a SqlitePool,
@@ -26,7 +28,7 @@ impl<'a> Logins<'a> {
) -> Result<Option<String>, BoxedError> {
let mut tx = self.db.begin().await?;
- let login = if let Some((login, stored_hash)) = tx.logins().for_login(name).await? {
+ let login = if let Some((login, stored_hash)) = tx.auth().for_name(name).await? {
if stored_hash.verify(password)? {
// Password verified; use the login.
Some(login)
@@ -75,31 +77,3 @@ impl<'a> Logins<'a> {
Ok(())
}
}
-
-#[derive(Debug, sqlx::Type)]
-#[sqlx(transparent)]
-pub struct StoredHash(String);
-
-impl StoredHash {
- fn new(password: &str) -> Result<Self, password_hash::Error> {
- let salt = SaltString::generate(&mut OsRng);
- let argon2 = Argon2::default();
- let hash = argon2
- .hash_password(password.as_bytes(), &salt)?
- .to_string();
- Ok(Self(hash))
- }
-
- fn verify(&self, password: &str) -> Result<bool, password_hash::Error> {
- let hash = PasswordHash::new(&self.0)?;
-
- match Argon2::default().verify_password(password.as_bytes(), &hash) {
- // Successful authentication, not an error
- Ok(()) => Ok(true),
- // Unsuccessful authentication, also not an error
- Err(password_hash::errors::Error::Password) => Ok(false),
- // Password validation failed for some other reason, treat as an error
- Err(err) => Err(err),
- }
- }
-}
diff --git a/src/login/extract/identity_token.rs b/src/login/extract.rs
index c813324..735bc22 100644
--- a/src/login/extract/identity_token.rs
+++ b/src/login/extract.rs
@@ -1,5 +1,3 @@
-use std::convert::Infallible;
-
use axum::{
extract::FromRequestParts,
http::request::Parts,
diff --git a/src/login/extract/mod.rs b/src/login/extract/mod.rs
deleted file mode 100644
index ba943a6..0000000
--- a/src/login/extract/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-mod identity_token;
-mod login;
-
-pub use self::identity_token::IdentityToken;
diff --git a/src/login/mod.rs b/src/login/mod.rs
index 5070301..191cce0 100644
--- a/src/login/mod.rs
+++ b/src/login/mod.rs
@@ -1,6 +1,6 @@
pub use self::routes::router;
pub mod app;
-mod extract;
-pub mod repo;
+pub mod extract;
+mod repo;
mod routes;
diff --git a/src/login/repo/auth.rs b/src/login/repo/auth.rs
new file mode 100644
index 0000000..78b44f0
--- /dev/null
+++ b/src/login/repo/auth.rs
@@ -0,0 +1,53 @@
+use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
+
+use crate::{
+ password::StoredHash,
+ repo::login::{self, Login},
+};
+
+pub trait Provider {
+ fn auth(&mut self) -> Auth;
+}
+
+impl<'c> Provider for Transaction<'c, Sqlite> {
+ fn auth(&mut self) -> Auth {
+ Auth(self)
+ }
+}
+
+pub struct Auth<'t>(&'t mut SqliteConnection);
+
+impl<'t> Auth<'t> {
+ /// Retrieves a login by name, plus its stored password hash for
+ /// verification. If there's no login with the requested name, this will
+ /// return [None].
+ pub async fn for_name(
+ &mut self,
+ name: &str,
+ ) -> Result<Option<(Login, StoredHash)>, sqlx::Error> {
+ let found = sqlx::query!(
+ r#"
+ select
+ id as "id: login::Id",
+ name,
+ password_hash as "password_hash: StoredHash"
+ from login
+ where name = $1
+ "#,
+ name,
+ )
+ .map(|rec| {
+ (
+ Login {
+ id: rec.id,
+ name: rec.name,
+ },
+ rec.password_hash,
+ )
+ })
+ .fetch_optional(&mut *self.0)
+ .await?;
+
+ Ok(found)
+ }
+}
diff --git a/src/login/repo/mod.rs b/src/login/repo/mod.rs
index 07da569..0e4a05d 100644
--- a/src/login/repo/mod.rs
+++ b/src/login/repo/mod.rs
@@ -1,2 +1 @@
-pub mod logins;
-pub mod tokens;
+pub mod auth;
diff --git a/src/password.rs b/src/password.rs
new file mode 100644
index 0000000..b14f728
--- /dev/null
+++ b/src/password.rs
@@ -0,0 +1,31 @@
+use argon2::Argon2;
+use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
+use rand_core::OsRng;
+
+#[derive(Debug, sqlx::Type)]
+#[sqlx(transparent)]
+pub struct StoredHash(String);
+
+impl StoredHash {
+ pub fn new(password: &str) -> Result<Self, password_hash::Error> {
+ let salt = SaltString::generate(&mut OsRng);
+ let argon2 = Argon2::default();
+ let hash = argon2
+ .hash_password(password.as_bytes(), &salt)?
+ .to_string();
+ Ok(Self(hash))
+ }
+
+ pub fn verify(&self, password: &str) -> Result<bool, password_hash::Error> {
+ let hash = PasswordHash::new(&self.0)?;
+
+ match Argon2::default().verify_password(password.as_bytes(), &hash) {
+ // Successful authentication, not an error
+ Ok(()) => Ok(true),
+ // Unsuccessful authentication, also not an error
+ Err(password_hash::errors::Error::Password) => Ok(false),
+ // Password validation failed for some other reason, treat as an error
+ Err(err) => Err(err),
+ }
+ }
+}
diff --git a/src/channel/repo/channels.rs b/src/repo/channel.rs
index ab7489c..ab7489c 100644
--- a/src/channel/repo/channels.rs
+++ b/src/repo/channel.rs
diff --git a/src/login/extract/login.rs b/src/repo/login/extract.rs
index 8b5bb41..a068bc0 100644
--- a/src/login/extract/login.rs
+++ b/src/repo/login/extract.rs
@@ -4,12 +4,8 @@ use axum::{
response::{IntoResponse, Response},
};
-use crate::{
- app::App,
- clock::RequestedAt,
- error::InternalError,
- login::{extract::IdentityToken, repo::logins::Login},
-};
+use super::Login;
+use crate::{app::App, clock::RequestedAt, error::InternalError, login::extract::IdentityToken};
#[async_trait::async_trait]
impl FromRequestParts<App> for Login {
diff --git a/src/repo/login/mod.rs b/src/repo/login/mod.rs
new file mode 100644
index 0000000..e23a7b7
--- /dev/null
+++ b/src/repo/login/mod.rs
@@ -0,0 +1,4 @@
+mod extract;
+mod store;
+
+pub use self::store::{Id, Login, Logins, Provider};
diff --git a/src/login/repo/logins.rs b/src/repo/login/store.rs
index 11ae50f..24dd744 100644
--- a/src/login/repo/logins.rs
+++ b/src/repo/login/store.rs
@@ -1,7 +1,6 @@
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
-use crate::id::Id as BaseId;
-use crate::login::app::StoredHash;
+use crate::{id::Id as BaseId, password::StoredHash};
pub trait Provider {
fn logins(&mut self) -> Logins;
@@ -15,8 +14,10 @@ impl<'c> Provider for Transaction<'c, Sqlite> {
pub struct Logins<'t>(&'t mut SqliteConnection);
-// This also implements FromRequestParts (see `src/login/extract/login.rs`). As
-// a result, it can be used as an extractor.
+// 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, serde::Serialize)]
pub struct Login {
pub id: Id,
@@ -71,39 +72,6 @@ impl<'c> Logins<'c> {
Ok(login)
}
-
- /// Retrieves a login by name, plus its stored password hash for
- /// verification. If there's no login with the requested name, this will
- /// return [None].
- pub async fn for_login(
- &mut self,
- name: &str,
- ) -> Result<Option<(Login, StoredHash)>, sqlx::Error> {
- let found = sqlx::query!(
- r#"
- select
- id as "id: Id",
- name,
- password_hash as "password_hash: StoredHash"
- from login
- where name = $1
- "#,
- name,
- )
- .map(|rec| {
- (
- Login {
- id: rec.id,
- name: rec.name,
- },
- rec.password_hash,
- )
- })
- .fetch_optional(&mut *self.0)
- .await?;
-
- Ok(found)
- }
}
impl<'t> From<&'t mut SqliteConnection> for Logins<'t> {
diff --git a/src/repo/message.rs b/src/repo/message.rs
new file mode 100644
index 0000000..e331a4e
--- /dev/null
+++ b/src/repo/message.rs
@@ -0,0 +1,27 @@
+use std::fmt;
+
+use crate::id::Id as BaseId;
+
+/// Stable identifier for a [Message]. Prefixed with `M`.
+#[derive(Clone, Debug, Eq, Hash, PartialEq, sqlx::Type, serde::Deserialize, serde::Serialize)]
+#[sqlx(transparent)]
+#[serde(transparent)]
+pub struct Id(BaseId);
+
+impl From<BaseId> for Id {
+ fn from(id: BaseId) -> Self {
+ Self(id)
+ }
+}
+
+impl Id {
+ pub fn generate() -> Self {
+ BaseId::generate("M")
+ }
+}
+
+impl fmt::Display for Id {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
diff --git a/src/repo/mod.rs b/src/repo/mod.rs
new file mode 100644
index 0000000..d8995a3
--- /dev/null
+++ b/src/repo/mod.rs
@@ -0,0 +1,4 @@
+pub mod channel;
+pub mod login;
+pub mod message;
+pub mod token;
diff --git a/src/login/repo/tokens.rs b/src/repo/token.rs
index ec95f6a..e7eb273 100644
--- a/src/login/repo/tokens.rs
+++ b/src/repo/token.rs
@@ -2,7 +2,7 @@ use chrono::TimeDelta;
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
use uuid::Uuid;
-use super::logins::{Id as LoginId, Login};
+use super::login::{self, Login};
use crate::clock::DateTime;
pub trait Provider {
@@ -22,7 +22,7 @@ impl<'c> Tokens<'c> {
/// be used to control expiry, until the token is actually used.
pub async fn issue(
&mut self,
- login: &LoginId,
+ login: &login::Id,
issued_at: DateTime,
) -> Result<String, sqlx::Error> {
let secret = Uuid::new_v4().to_string();
@@ -109,7 +109,7 @@ impl<'c> Tokens<'c> {
Login,
r#"
select
- login.id as "id: LoginId",
+ login.id as "id: login::Id",
name
from login
join token on login.id = token.login