summaryrefslogtreecommitdiff
path: root/src/setup
diff options
context:
space:
mode:
Diffstat (limited to 'src/setup')
-rw-r--r--src/setup/app.rs62
-rw-r--r--src/setup/middleware.rs16
-rw-r--r--src/setup/mod.rs6
-rw-r--r--src/setup/repo.rs28
-rw-r--r--src/setup/routes.rs50
5 files changed, 162 insertions, 0 deletions
diff --git a/src/setup/app.rs b/src/setup/app.rs
new file mode 100644
index 0000000..24e0010
--- /dev/null
+++ b/src/setup/app.rs
@@ -0,0 +1,62 @@
+use sqlx::sqlite::SqlitePool;
+
+use super::repo::Provider as _;
+use crate::{
+ clock::DateTime,
+ event::{repo::Provider as _, Broadcaster, Event},
+ login::{repo::Provider as _, Password},
+ token::{repo::Provider as _, Secret},
+};
+
+pub struct Setup<'a> {
+ db: &'a SqlitePool,
+ events: &'a Broadcaster,
+}
+
+impl<'a> Setup<'a> {
+ pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self {
+ Self { db, events }
+ }
+
+ pub async fn initial(
+ &self,
+ name: &str,
+ password: &Password,
+ created_at: &DateTime,
+ ) -> Result<Secret, Error> {
+ let password_hash = password.hash()?;
+
+ let mut tx = self.db.begin().await?;
+ let login = if tx.setup().completed().await? {
+ Err(Error::SetupCompleted)?
+ } else {
+ let created = tx.sequence().next(created_at).await?;
+ tx.logins().create(name, &password_hash, &created).await?
+ };
+ let secret = tx.tokens().issue(&login, created_at).await?;
+ tx.commit().await?;
+
+ self.events
+ .broadcast(login.events().map(Event::from).collect::<Vec<_>>());
+
+ Ok(secret)
+ }
+
+ pub async fn completed(&self) -> Result<bool, sqlx::Error> {
+ let mut tx = self.db.begin().await?;
+ let completed = tx.setup().completed().await?;
+ tx.commit().await?;
+
+ Ok(completed)
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error("initial setup previously completed")]
+ SetupCompleted,
+ #[error(transparent)]
+ Database(#[from] sqlx::Error),
+ #[error(transparent)]
+ PasswordHash(#[from] password_hash::Error),
+}
diff --git a/src/setup/middleware.rs b/src/setup/middleware.rs
new file mode 100644
index 0000000..a5f9070
--- /dev/null
+++ b/src/setup/middleware.rs
@@ -0,0 +1,16 @@
+use axum::{
+ extract::{Request, State},
+ http::StatusCode,
+ middleware::Next,
+ response::{IntoResponse, Response},
+};
+
+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(),
+ Err(error) => Internal::from(error).into_response(),
+ }
+}
diff --git a/src/setup/mod.rs b/src/setup/mod.rs
new file mode 100644
index 0000000..5a8fa37
--- /dev/null
+++ b/src/setup/mod.rs
@@ -0,0 +1,6 @@
+pub mod app;
+pub mod middleware;
+pub mod repo;
+mod routes;
+
+pub use self::routes::router;
diff --git a/src/setup/repo.rs b/src/setup/repo.rs
new file mode 100644
index 0000000..de93f51
--- /dev/null
+++ b/src/setup/repo.rs
@@ -0,0 +1,28 @@
+use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
+
+pub trait Provider {
+ fn setup(&mut self) -> Setup;
+}
+
+impl<'c> Provider for Transaction<'c, Sqlite> {
+ fn setup(&mut self) -> Setup {
+ Setup(self)
+ }
+}
+
+pub struct Setup<'t>(&'t mut SqliteConnection);
+
+impl<'c> Setup<'c> {
+ pub async fn completed(&mut self) -> Result<bool, sqlx::Error> {
+ let completed = sqlx::query_scalar!(
+ r#"
+ select count(*) > 0 as "completed: bool"
+ from login
+ "#,
+ )
+ .fetch_one(&mut *self.0)
+ .await?;
+
+ Ok(completed)
+ }
+}
diff --git a/src/setup/routes.rs b/src/setup/routes.rs
new file mode 100644
index 0000000..ff41734
--- /dev/null
+++ b/src/setup/routes.rs
@@ -0,0 +1,50 @@
+use axum::{
+ extract::{Json, State},
+ http::StatusCode,
+ response::{IntoResponse, Response},
+ routing::post,
+ Router,
+};
+
+use super::app;
+use crate::{
+ app::App, clock::RequestedAt, error::Internal, login::Password, token::extract::IdentityToken,
+};
+
+pub fn router() -> Router<App> {
+ Router::new().route("/api/setup", post(on_setup))
+}
+
+#[derive(serde::Deserialize)]
+struct SetupRequest {
+ name: String,
+ password: Password,
+}
+
+async fn on_setup(
+ State(app): State<App>,
+ RequestedAt(setup_at): RequestedAt,
+ identity: IdentityToken,
+ Json(request): Json<SetupRequest>,
+) -> Result<(IdentityToken, StatusCode), SetupError> {
+ let secret = app
+ .setup()
+ .initial(&request.name, &request.password, &setup_at)
+ .await
+ .map_err(SetupError)?;
+ let identity = identity.set(secret);
+ Ok((identity, StatusCode::NO_CONTENT))
+}
+
+#[derive(Debug)]
+struct SetupError(app::Error);
+
+impl IntoResponse for SetupError {
+ fn into_response(self) -> Response {
+ let Self(error) = self;
+ match error {
+ app::Error::SetupCompleted => (StatusCode::CONFLICT, error.to_string()).into_response(),
+ other => Internal::from(other).into_response(),
+ }
+ }
+}