summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-10-11 22:57:56 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-10-11 22:57:56 -0400
commit756863f298f9e4277863f9e8758e253c5ae95923 (patch)
tree82a9c6643b360b035f4630f2b1db138d9937c8d8 /src
parent812f1cafe3f8a68bf45b677fade7417c30d92eac (diff)
Return a distinct error when an invite username is in use.
I've also aligned channel creation with this (it's 409 Conflict). To make server setup more distinct, it now returns 503 Service Unavailable if setup has not been completed.
Diffstat (limited to 'src')
-rw-r--r--src/channel/app.rs16
-rw-r--r--src/channel/routes.rs2
-rw-r--r--src/db/mod.rs30
-rw-r--r--src/invite/app.rs10
-rw-r--r--src/invite/routes.rs3
-rw-r--r--src/setup/middleware.rs6
6 files changed, 49 insertions, 18 deletions
diff --git a/src/channel/app.rs b/src/channel/app.rs
index 7c0b107..5d6cada 100644
--- a/src/channel/app.rs
+++ b/src/channel/app.rs
@@ -5,7 +5,7 @@ use sqlx::sqlite::SqlitePool;
use super::{repo::Provider as _, Channel, History, Id};
use crate::{
clock::DateTime,
- db::NotFound,
+ db::{Duplicate as _, NotFound as _},
event::{repo::Provider as _, Broadcaster, Event, Sequence},
message::repo::Provider as _,
};
@@ -27,7 +27,7 @@ impl<'a> Channels<'a> {
.channels()
.create(name, &created)
.await
- .map_err(|err| CreateError::from_duplicate_name(err, name))?;
+ .duplicate(|| CreateError::DuplicateName(name.into()))?;
tx.commit().await?;
self.events
@@ -133,18 +133,6 @@ pub enum Error {
DatabaseError(#[from] sqlx::Error),
}
-impl CreateError {
- fn from_duplicate_name(error: sqlx::Error, name: &str) -> Self {
- if let Some(error) = error.as_database_error() {
- if error.is_unique_violation() {
- return Self::DuplicateName(name.into());
- }
- }
-
- Self::from(error)
- }
-}
-
#[derive(Debug, thiserror::Error)]
pub enum InternalError {
#[error(transparent)]
diff --git a/src/channel/routes.rs b/src/channel/routes.rs
index e97c447..eaf7962 100644
--- a/src/channel/routes.rs
+++ b/src/channel/routes.rs
@@ -53,7 +53,7 @@ impl IntoResponse for CreateError {
let Self(error) = self;
match error {
duplicate @ app::CreateError::DuplicateName(_) => {
- (StatusCode::BAD_REQUEST, duplicate.to_string()).into_response()
+ (StatusCode::CONFLICT, duplicate.to_string()).into_response()
}
other => Internal::from(other).into_response(),
}
diff --git a/src/db/mod.rs b/src/db/mod.rs
index fa3d74e..6005813 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -4,6 +4,7 @@ use std::str::FromStr;
use hex_literal::hex;
use sqlx::{
+ error::{DatabaseError, ErrorKind},
migrate::{Migrate as _, MigrateDatabase as _},
sqlite::{Sqlite, SqliteConnectOptions, SqlitePool, SqlitePoolOptions},
};
@@ -161,3 +162,32 @@ impl<T> NotFound for Result<T, sqlx::Error> {
self.optional()?.ok_or_else(map)
}
}
+
+pub trait Duplicate {
+ type Ok;
+ type Error;
+
+ fn duplicate<E, F>(self, map: F) -> Result<Self::Ok, E>
+ where
+ E: From<Self::Error>,
+ F: FnOnce() -> E;
+}
+
+impl<T> Duplicate for Result<T, sqlx::Error> {
+ type Ok = T;
+ type Error = sqlx::Error;
+
+ fn duplicate<E, F>(self, map: F) -> Result<T, E>
+ where
+ E: From<sqlx::Error>,
+ F: FnOnce() -> E,
+ {
+ match self {
+ Ok(value) => Ok(value),
+ Err(error) => match error.as_database_error().map(DatabaseError::kind) {
+ Some(ErrorKind::UniqueViolation) => Err(map()),
+ _ => Err(error.into()),
+ },
+ }
+ }
+}
diff --git a/src/invite/app.rs b/src/invite/app.rs
index 998b4f1..6800d72 100644
--- a/src/invite/app.rs
+++ b/src/invite/app.rs
@@ -4,7 +4,7 @@ use sqlx::sqlite::SqlitePool;
use super::{repo::Provider as _, Id, Invite, Summary};
use crate::{
clock::DateTime,
- db::NotFound as _,
+ db::{Duplicate as _, NotFound as _},
event::repo::Provider as _,
login::{repo::Provider as _, Login, Password},
token::{repo::Provider as _, Secret},
@@ -68,7 +68,11 @@ impl<'a> Invites<'a> {
// catch it.
tx.invites().accept(&invite).await?;
let created = tx.sequence().next(accepted_at).await?;
- let login = tx.logins().create(name, &password_hash, &created).await?;
+ let login = tx
+ .logins()
+ .create(name, &password_hash, &created)
+ .await
+ .duplicate(|| AcceptError::DuplicateLogin(name.into()))?;
let secret = tx.tokens().issue(&login, accepted_at).await?;
tx.commit().await?;
@@ -99,6 +103,8 @@ pub enum Error {
pub enum AcceptError {
#[error("invite not found: {0}")]
NotFound(Id),
+ #[error("name in use: {0}")]
+ DuplicateLogin(String),
#[error(transparent)]
Database(#[from] sqlx::Error),
#[error(transparent)]
diff --git a/src/invite/routes.rs b/src/invite/routes.rs
index 3384e10..977fe9b 100644
--- a/src/invite/routes.rs
+++ b/src/invite/routes.rs
@@ -88,6 +88,9 @@ impl IntoResponse for AcceptError {
let Self(error) = self;
match error {
error @ app::AcceptError::NotFound(_) => NotFound(error).into_response(),
+ error @ app::AcceptError::DuplicateLogin(_) => {
+ (StatusCode::CONFLICT, error.to_string()).into_response()
+ }
other => Internal::from(other).into_response(),
}
}
diff --git a/src/setup/middleware.rs b/src/setup/middleware.rs
index a5f9070..5f9996b 100644
--- a/src/setup/middleware.rs
+++ b/src/setup/middleware.rs
@@ -10,7 +10,11 @@ use crate::{app::App, error::Internal};
pub async fn setup_required(State(app): State<App>, request: Request, next: Next) -> Response {
match app.setup().completed().await {
Ok(true) => next.run(request).await,
- Ok(false) => (StatusCode::CONFLICT, "initial setup not completed").into_response(),
+ Ok(false) => (
+ StatusCode::SERVICE_UNAVAILABLE,
+ "initial setup not completed",
+ )
+ .into_response(),
Err(error) => Internal::from(error).into_response(),
}
}