diff options
| -rw-r--r-- | .sqlx/query-65d3e876d3a51752ff63a5ab71cc7531cc00eac44eda227cb0321d1d417f975e.json | 26 | ||||
| -rw-r--r-- | .sqlx/query-a9203ebdb9c57e59ae79ae5932f00e82f9dd4ad8a5abf3e905db36d3f9a06712.json (renamed from .sqlx/query-67a4bc5f758852e19c50fab42d3fa9460e220c662d83c8af6e3412a317076e4d.json) | 12 | ||||
| -rw-r--r-- | .sqlx/query-c0b5b8401995ea7ae491946617e402396d761dc5c1d3ffde85e5f8fe6cce5a6b.json (renamed from .sqlx/query-07dc90365168e34f2640bb55c2ca8b868804d4dfcf5d4bdb55d0db9ba883f7a8.json) | 10 | ||||
| -rw-r--r-- | src/index.rs | 54 | ||||
| -rw-r--r-- | src/login/extract/identity_token.rs (renamed from src/login/extract.rs) | 0 | ||||
| -rw-r--r-- | src/login/extract/login.rs | 61 | ||||
| -rw-r--r-- | src/login/extract/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/mod.rs | 2 | ||||
| -rw-r--r-- | src/login/repo/logins.rs | 27 | ||||
| -rw-r--r-- | src/login/repo/tokens.rs | 29 |
10 files changed, 187 insertions, 38 deletions
diff --git a/.sqlx/query-65d3e876d3a51752ff63a5ab71cc7531cc00eac44eda227cb0321d1d417f975e.json b/.sqlx/query-65d3e876d3a51752ff63a5ab71cc7531cc00eac44eda227cb0321d1d417f975e.json new file mode 100644 index 0000000..8011996 --- /dev/null +++ b/.sqlx/query-65d3e876d3a51752ff63a5ab71cc7531cc00eac44eda227cb0321d1d417f975e.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n select\n login.id as \"id: LoginId\",\n name\n from login\n join token on login.id = token.login\n where token.secret = $1\n ", + "describe": { + "columns": [ + { + "name": "id: LoginId", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "65d3e876d3a51752ff63a5ab71cc7531cc00eac44eda227cb0321d1d417f975e" +} diff --git a/.sqlx/query-67a4bc5f758852e19c50fab42d3fa9460e220c662d83c8af6e3412a317076e4d.json b/.sqlx/query-a9203ebdb9c57e59ae79ae5932f00e82f9dd4ad8a5abf3e905db36d3f9a06712.json index a4dffb5..8c6e9f2 100644 --- a/.sqlx/query-67a4bc5f758852e19c50fab42d3fa9460e220c662d83c8af6e3412a317076e4d.json +++ b/.sqlx/query-a9203ebdb9c57e59ae79ae5932f00e82f9dd4ad8a5abf3e905db36d3f9a06712.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n password_hash as \"password_hash: StoredHash\"\n from login\n where name = $1\n ", + "query": "\n select\n id as \"id: Id\",\n name,\n password_hash as \"password_hash: StoredHash\"\n from login\n where name = $1\n ", "describe": { "columns": [ { @@ -9,9 +9,14 @@ "type_info": "Text" }, { - "name": "password_hash: StoredHash", + "name": "name", "ordinal": 1, "type_info": "Text" + }, + { + "name": "password_hash: StoredHash", + "ordinal": 2, + "type_info": "Text" } ], "parameters": { @@ -19,8 +24,9 @@ }, "nullable": [ false, + false, false ] }, - "hash": "67a4bc5f758852e19c50fab42d3fa9460e220c662d83c8af6e3412a317076e4d" + "hash": "a9203ebdb9c57e59ae79ae5932f00e82f9dd4ad8a5abf3e905db36d3f9a06712" } diff --git a/.sqlx/query-07dc90365168e34f2640bb55c2ca8b868804d4dfcf5d4bdb55d0db9ba883f7a8.json b/.sqlx/query-c0b5b8401995ea7ae491946617e402396d761dc5c1d3ffde85e5f8fe6cce5a6b.json index 91a1e49..66aefac 100644 --- a/.sqlx/query-07dc90365168e34f2640bb55c2ca8b868804d4dfcf5d4bdb55d0db9ba883f7a8.json +++ b/.sqlx/query-c0b5b8401995ea7ae491946617e402396d761dc5c1d3ffde85e5f8fe6cce5a6b.json @@ -1,20 +1,26 @@ { "db_name": "SQLite", - "query": "\n insert or fail\n into login (id, name, password_hash)\n values ($1, $2, $3)\n returning id as \"id: Id\"\n ", + "query": "\n insert or fail\n into login (id, name, password_hash)\n values ($1, $2, $3)\n returning id as \"id: Id\", name\n ", "describe": { "columns": [ { "name": "id: Id", "ordinal": 0, "type_info": "Text" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" } ], "parameters": { "Right": 3 }, "nullable": [ + false, false ] }, - "hash": "07dc90365168e34f2640bb55c2ca8b868804d4dfcf5d4bdb55d0db9ba883f7a8" + "hash": "c0b5b8401995ea7ae491946617e402396d761dc5c1d3ffde85e5f8fe6cce5a6b" } diff --git a/src/index.rs b/src/index.rs index 98eb47f..a716af2 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,40 +1,56 @@ use axum::{response::IntoResponse, routing::get, Router}; +use sqlx::sqlite::SqlitePool; -pub fn router<S>() -> Router<S> -where - S: Send + Sync + Clone + 'static, -{ +use crate::login::repo::logins::Login; + +pub fn router() -> Router<SqlitePool> { Router::new().route("/", get(index)) } -async fn index() -> impl IntoResponse { - templates::index() +async fn index(login: Option<Login>) -> impl IntoResponse { + templates::index(login) } mod templates { use maud::{html, Markup, DOCTYPE}; - pub fn index() -> Markup { + + use crate::login::repo::logins::Login; + + pub fn index(login: Option<Login>) -> Markup { html! { (DOCTYPE) head { title { "hi" } } body { - form action="/login" method="post" { - label { - "name" - input name="name" type="text" {} - } - label { - "password" - input name="password" type="password" {} - } - button { "hi" } + @match login { + None => { (login_form()) } + Some(login) => { (logout_form(&login.name)) } } + } + } + } - form action="/logout" method="post" { - button { "bye" } + fn login_form() -> Markup { + html! { + form action="/login" method="post" { + label { + "name" + input name="name" type="text" {} } + label { + "password" + input name="password" type="password" {} + } + button { "hi" } + } + } + } + + fn logout_form(name: &str) -> Markup { + html! { + form action="/logout" method="post" { + button { "bye, " (name) } } } } diff --git a/src/login/extract.rs b/src/login/extract/identity_token.rs index d39e3df..d39e3df 100644 --- a/src/login/extract.rs +++ b/src/login/extract/identity_token.rs diff --git a/src/login/extract/login.rs b/src/login/extract/login.rs new file mode 100644 index 0000000..f49933a --- /dev/null +++ b/src/login/extract/login.rs @@ -0,0 +1,61 @@ +use axum::{ + extract::{FromRequestParts, State}, + http::{request::Parts, StatusCode}, + response::{IntoResponse, Response}, +}; +use sqlx::sqlite::SqlitePool; + +use crate::{ + error::InternalError, + login::{ + extract::IdentityToken, + repo::{logins::Login, tokens::Provider as _}, + }, +}; + +#[async_trait::async_trait] +impl FromRequestParts<SqlitePool> for Login { + type Rejection = LoginError<InternalError>; + + async fn from_request_parts( + parts: &mut Parts, + state: &SqlitePool, + ) -> Result<Self, Self::Rejection> { + let identity_token = IdentityToken::from_request_parts(parts, state).await?; + + let token = identity_token.token().ok_or(LoginError::Forbidden)?; + + let db = State::<SqlitePool>::from_request_parts(parts, state).await?; + let mut tx = db.begin().await?; + let login = tx.tokens().validate(token).await?; + tx.commit().await?; + + login.ok_or(LoginError::Forbidden) + } +} + +pub enum LoginError<E> { + Failure(E), + Forbidden, +} + +impl<E> IntoResponse for LoginError<E> +where + E: IntoResponse, +{ + fn into_response(self) -> Response { + match self { + Self::Forbidden => (StatusCode::FORBIDDEN, "forbidden").into_response(), + Self::Failure(e) => e.into_response(), + } + } +} + +impl<E> From<E> for LoginError<InternalError> +where + E: Into<InternalError>, +{ + fn from(err: E) -> Self { + Self::Failure(err.into()) + } +} diff --git a/src/login/extract/mod.rs b/src/login/extract/mod.rs new file mode 100644 index 0000000..ba943a6 --- /dev/null +++ b/src/login/extract/mod.rs @@ -0,0 +1,4 @@ +mod identity_token; +mod login; + +pub use self::identity_token::IdentityToken; diff --git a/src/login/mod.rs b/src/login/mod.rs index 4074359..c2b2924 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -1,5 +1,5 @@ pub use self::routes::router; mod extract; -mod repo; +pub mod repo; mod routes; diff --git a/src/login/repo/logins.rs b/src/login/repo/logins.rs index c6db86e..84b4bf8 100644 --- a/src/login/repo/logins.rs +++ b/src/login/repo/logins.rs @@ -16,19 +16,17 @@ impl<'c> Provider for Transaction<'c, Sqlite> { } } +// This also implements FromRequestParts (see `src/login/extract/login.rs`). As +// a result, it can be used as an extractor. pub struct Logins<'t>(&'t mut SqliteConnection); #[derive(Debug)] pub struct Login { pub id: Id, - // Field unused (as of this writing), omitted to avoid warnings. - // Feel free to add it: - // - // pub name: String, - - // However, the omission of the hashed password is deliberate, to minimize - // the chance that it ends up tangled up in debug output or in some other - // chunk of logic elsewhere. + pub name: String, + // The omission of the hashed password is deliberate, to minimize the + // chance that it ends up tangled up in debug output or in some other chunk + // of logic elsewhere. } impl<'c> Logins<'c> { @@ -49,7 +47,7 @@ impl<'c> Logins<'c> { insert or fail into login (id, name, password_hash) values ($1, $2, $3) - returning id as "id: Id" + returning id as "id: Id", name "#, id, name, @@ -107,13 +105,22 @@ impl<'c> Logins<'c> { r#" select id as "id: Id", + name, password_hash as "password_hash: StoredHash" from login where name = $1 "#, name, ) - .map(|rec| (Login { id: rec.id }, rec.password_hash)) + .map(|rec| { + ( + Login { + id: rec.id, + name: rec.name, + }, + rec.password_hash, + ) + }) .fetch_optional(&mut *self.0) .await?; diff --git a/src/login/repo/tokens.rs b/src/login/repo/tokens.rs index e31a301..584f6dc 100644 --- a/src/login/repo/tokens.rs +++ b/src/login/repo/tokens.rs @@ -1,7 +1,7 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use uuid::Uuid; -use super::logins::Id as LoginId; +use super::logins::{Id as LoginId, Login}; use crate::error::BoxedError; type DateTime = chrono::DateTime<chrono::Utc>; @@ -45,18 +45,41 @@ impl<'c> Tokens<'c> { Ok(secret) } - pub async fn revoke(&mut self, token: &str) -> Result<(), BoxedError> { + /// Revoke a token by its secret. If there is no such token with that + /// secret, this will succeed by doing nothing. + pub async fn revoke(&mut self, secret: &str) -> Result<(), BoxedError> { sqlx::query!( r#" delete from token where secret = $1 "#, - token, + secret, ) .execute(&mut *self.0) .await?; Ok(()) } + + /// Validate a token by its secret, retrieving the associated Login record. + /// Will return [None] if the token is not valid. + pub async fn validate(&mut self, secret: &str) -> Result<Option<Login>, BoxedError> { + let login = sqlx::query_as!( + Login, + r#" + select + login.id as "id: LoginId", + name + from login + join token on login.id = token.login + where token.secret = $1 + "#, + secret, + ) + .fetch_optional(&mut *self.0) + .await?; + + Ok(login) + } } |
