summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.sqlx/query-65d3e876d3a51752ff63a5ab71cc7531cc00eac44eda227cb0321d1d417f975e.json26
-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.rs54
-rw-r--r--src/login/extract/identity_token.rs (renamed from src/login/extract.rs)0
-rw-r--r--src/login/extract/login.rs61
-rw-r--r--src/login/extract/mod.rs4
-rw-r--r--src/login/mod.rs2
-rw-r--r--src/login/repo/logins.rs27
-rw-r--r--src/login/repo/tokens.rs29
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)
+ }
}