diff options
Diffstat (limited to 'src')
| -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 |
7 files changed, 144 insertions, 33 deletions
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) + } } |
