summaryrefslogtreecommitdiff
path: root/src/login/routes.rs
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-09-03 01:25:20 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-09-03 02:09:25 -0400
commitb404344a7c4ab5cb6c7d7b445fab796be79b848f (patch)
treec476b125316b9d4aa7bdece7c9bb8e2f65d2961e /src/login/routes.rs
parent92a7518975c6bc4b2f9b9c6c12c458b24e8cfaf5 (diff)
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.
Diffstat (limited to 'src/login/routes.rs')
-rw-r--r--src/login/routes.rs78
1 files changed, 78 insertions, 0 deletions
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<SqlitePool> {
+ Router::new().route("/login", post(on_login))
+}
+
+#[derive(serde::Deserialize)]
+struct Login {
+ name: String,
+ password: String,
+}
+
+async fn on_login(
+ State(db): State<SqlitePool>,
+ cookies: CookieJar,
+ Form(form): Form<Login>,
+) -> Result<impl IntoResponse, InternalError> {
+ 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)
+}