summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.sqlx/query-57f1b5546a9071467b2f78f3384b853dcb23d73ef05398cab4d6767be9fb3d50.json12
-rw-r--r--Cargo.lock5
-rw-r--r--Cargo.toml1
-rw-r--r--src/index.rs4
-rw-r--r--src/login/extract.rs69
-rw-r--r--src/login/mod.rs1
-rw-r--r--src/login/repo/tokens.rs15
-rw-r--r--src/login/routes.rs43
8 files changed, 136 insertions, 14 deletions
diff --git a/.sqlx/query-57f1b5546a9071467b2f78f3384b853dcb23d73ef05398cab4d6767be9fb3d50.json b/.sqlx/query-57f1b5546a9071467b2f78f3384b853dcb23d73ef05398cab4d6767be9fb3d50.json
new file mode 100644
index 0000000..464a475
--- /dev/null
+++ b/.sqlx/query-57f1b5546a9071467b2f78f3384b853dcb23d73ef05398cab4d6767be9fb3d50.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n delete\n from token\n where secret = $1\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "57f1b5546a9071467b2f78f3384b853dcb23d73ef05398cab4d6767be9fb3d50"
+}
diff --git a/Cargo.lock b/Cargo.lock
index a05554d..687863e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -113,9 +113,9 @@ dependencies = [
[[package]]
name = "async-trait"
-version = "0.1.81"
+version = "0.1.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
+checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
dependencies = [
"proc-macro2",
"quote",
@@ -707,6 +707,7 @@ name = "hi"
version = "0.1.0"
dependencies = [
"argon2",
+ "async-trait",
"axum",
"axum-extra",
"chrono",
diff --git a/Cargo.toml b/Cargo.toml
index 9f7aa4a..eb748ba 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ edition = "2021"
[dependencies]
argon2 = "0.5.3"
+async-trait = "0.1.82"
axum = { version = "0.7.5", features = ["form"] }
axum-extra = { version = "0.9.3", features = ["cookie"] }
chrono = "0.4.38"
diff --git a/src/index.rs b/src/index.rs
index 6411ff4..98eb47f 100644
--- a/src/index.rs
+++ b/src/index.rs
@@ -31,6 +31,10 @@ mod templates {
}
button { "hi" }
}
+
+ form action="/logout" method="post" {
+ button { "bye" }
+ }
}
}
}
diff --git a/src/login/extract.rs b/src/login/extract.rs
new file mode 100644
index 0000000..d39e3df
--- /dev/null
+++ b/src/login/extract.rs
@@ -0,0 +1,69 @@
+use std::convert::Infallible;
+
+use axum::{
+ extract::FromRequestParts,
+ http::request::Parts,
+ response::{IntoResponseParts, ResponseParts},
+};
+use axum_extra::extract::cookie::{Cookie, CookieJar};
+
+// The usage pattern here - receive the extractor as an argument, return it in
+// the response - is heavily modelled after CookieJar's own intended usage.
+pub struct IdentityToken {
+ cookies: CookieJar,
+}
+
+impl IdentityToken {
+ /// Get the identity token sent in the request, if any. If the identity was
+ /// not sent, or if it has previously been [clear]ed, then this will return
+ /// [None]. If the identity has previously been [set], then this will return
+ /// that token.
+ pub fn token(&self) -> Option<&str> {
+ self.cookies.get(IDENTITY_COOKIE).map(Cookie::value)
+ }
+
+ /// Positively set the identity token, and ensure that it will be sent back
+ /// to the client when this extractor is included in a response.
+ pub fn set(self, token: &str) -> Self {
+ let identity_cookie = Cookie::build((IDENTITY_COOKIE, String::from(token)))
+ .http_only(true)
+ .permanent()
+ .build();
+
+ IdentityToken {
+ cookies: self.cookies.add(identity_cookie),
+ }
+ }
+
+ /// Remove the identity token and ensure that it will be cleared when this
+ /// extractor is included in a response.
+ pub fn clear(self) -> Self {
+ IdentityToken {
+ cookies: self.cookies.remove(IDENTITY_COOKIE),
+ }
+ }
+}
+
+const IDENTITY_COOKIE: &str = "identity";
+
+#[async_trait::async_trait]
+impl<S> FromRequestParts<S> for IdentityToken
+where
+ S: Send + Sync,
+{
+ type Rejection = Infallible;
+
+ async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
+ let cookies = CookieJar::from_request_parts(parts, state).await?;
+ Ok(IdentityToken { cookies })
+ }
+}
+
+impl IntoResponseParts for IdentityToken {
+ type Error = Infallible;
+
+ fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
+ let IdentityToken { cookies } = self;
+ cookies.into_response_parts(res)
+ }
+}
diff --git a/src/login/mod.rs b/src/login/mod.rs
index 8769407..4074359 100644
--- a/src/login/mod.rs
+++ b/src/login/mod.rs
@@ -1,4 +1,5 @@
pub use self::routes::router;
+mod extract;
mod repo;
mod routes;
diff --git a/src/login/repo/tokens.rs b/src/login/repo/tokens.rs
index 080e35a..e31a301 100644
--- a/src/login/repo/tokens.rs
+++ b/src/login/repo/tokens.rs
@@ -44,4 +44,19 @@ impl<'c> Tokens<'c> {
Ok(secret)
}
+
+ pub async fn revoke(&mut self, token: &str) -> Result<(), BoxedError> {
+ sqlx::query!(
+ r#"
+ delete
+ from token
+ where secret = $1
+ "#,
+ token,
+ )
+ .execute(&mut *self.0)
+ .await?;
+
+ Ok(())
+ }
}
diff --git a/src/login/routes.rs b/src/login/routes.rs
index c9def2a..a00982d 100644
--- a/src/login/routes.rs
+++ b/src/login/routes.rs
@@ -5,16 +5,20 @@ use axum::{
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 _};
+use super::{
+ extract::IdentityToken,
+ repo::{logins::Provider as _, tokens::Provider as _},
+};
pub fn router() -> Router<SqlitePool> {
- Router::new().route("/login", post(on_login))
+ Router::new()
+ .route("/login", post(on_login))
+ .route("/logout", post(on_logout))
}
#[derive(serde::Deserialize)]
@@ -25,10 +29,15 @@ struct Login {
async fn on_login(
State(db): State<SqlitePool>,
- cookies: CookieJar,
+ identity: IdentityToken,
Form(form): Form<Login>,
) -> Result<impl IntoResponse, InternalError> {
let now = Utc::now();
+
+ if identity.token().is_some() {
+ return Ok((StatusCode::BAD_REQUEST, identity, "already logged in"));
+ }
+
let mut tx = db.begin().await?;
// Spelling the following in the more conventional form,
@@ -59,20 +68,30 @@ async fn on_login(
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")
+ let identity = identity.set(&token);
+ (StatusCode::OK, identity, "logged in")
} else {
(
StatusCode::UNAUTHORIZED,
- cookies,
+ identity,
"invalid name or password",
)
};
Ok(resp)
}
+
+async fn on_logout(
+ State(db): State<SqlitePool>,
+ identity: IdentityToken,
+) -> Result<impl IntoResponse, InternalError> {
+ if let Some(token) = identity.token() {
+ let mut tx = db.begin().await?;
+ tx.tokens().revoke(token).await?;
+ tx.commit().await?;
+ }
+
+ let identity = identity.clear();
+
+ Ok((StatusCode::OK, identity, "logged out"))
+}