diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2024-10-11 20:55:36 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2024-10-11 20:55:36 -0400 |
| commit | 5ff106e910544788bc916626ae7665cb26e5af30 (patch) | |
| tree | f03f98677293a9d892e2d21d1a9a80aeedab60a3 /src/setup | |
| parent | d33c8af14c4adc1c15ab048299e06f9f35ae4de6 (diff) | |
Provide a separate "initial setup" endpoint that creates a user.
Diffstat (limited to 'src/setup')
| -rw-r--r-- | src/setup/app.rs | 62 | ||||
| -rw-r--r-- | src/setup/middleware.rs | 16 | ||||
| -rw-r--r-- | src/setup/mod.rs | 6 | ||||
| -rw-r--r-- | src/setup/repo.rs | 28 | ||||
| -rw-r--r-- | src/setup/routes.rs | 50 |
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(), + } + } +} |
