From b404344a7c4ab5cb6c7d7b445fab796be79b848f Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 3 Sep 2024 01:25:20 -0400 Subject: Allow login creation and authentication. This is a beefy change, as it adds a TON of smaller pieces needed to make this all function: * A database migration. * A ton of new crates for things like password validation, timekeeping, and HTML generation. * A first cut at a module structure for routes, templates, repositories. * A family of ID types, for identifying various kinds of domain thing. * AppError, which _doesn't_ implement Error but can be sent to clients. --- src/login/routes.rs | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/login/routes.rs (limited to 'src/login/routes.rs') diff --git a/src/login/routes.rs b/src/login/routes.rs new file mode 100644 index 0000000..c9def2a --- /dev/null +++ b/src/login/routes.rs @@ -0,0 +1,78 @@ +use axum::{ + extract::{Form, State}, + http::StatusCode, + response::IntoResponse, + routing::post, + Router, +}; +use axum_extra::extract::cookie::{Cookie, CookieJar}; +use chrono::Utc; +use sqlx::sqlite::SqlitePool; + +use crate::error::InternalError; + +use super::repo::{logins::Provider as _, tokens::Provider as _}; + +pub fn router() -> Router { + Router::new().route("/login", post(on_login)) +} + +#[derive(serde::Deserialize)] +struct Login { + name: String, + password: String, +} + +async fn on_login( + State(db): State, + cookies: CookieJar, + Form(form): Form, +) -> Result { + let now = Utc::now(); + let mut tx = db.begin().await?; + + // Spelling the following in the more conventional form, + // if let Some(…) = create().await? {} + // else if let Some(…) = validate().await? {} + // else {} + // pushes the specifics of whether the returned error types are Send or not + // (they aren't) into the type of this function's generated Futures, which + // in turn makes this function unusable as an Axum handler. + let login = tx.logins().create(&form.name, &form.password).await?; + let login = if login.is_some() { + login + } else { + tx.logins().authenticate(&form.name, &form.password).await? + }; + + // If `login` is Some, then we have an identity and can issue an identity + // token. If `login` is None, then neither creating a new login nor authenticating + // an existing one succeeded, and we must reject the attempt. + // + // These properties will be transferred to `token`, as well. + let token = if let Some(login) = login { + Some(tx.tokens().issue(&login.id, now).await?) + } else { + None + }; + + tx.commit().await?; + + let resp = if let Some(token) = token { + let cookie = Cookie::build(("identity", token)) + .http_only(true) + .permanent() + .build(); + let cookies = cookies.add(cookie); + + (StatusCode::OK, cookies, "logged in") + } else { + ( + StatusCode::UNAUTHORIZED, + cookies, + "invalid name or password", + ) + }; + + Ok(resp) +} -- cgit v1.2.3