non-working update to latest deps

This commit is contained in:
Joe Ardent 2023-12-10 13:52:27 -08:00
parent 404d0d6159
commit d4a684de29
4 changed files with 771 additions and 923 deletions

1210
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,12 +11,13 @@ optional_optional_user = {path = "optional_optional_user"}
# regular external deps # regular external deps
argon2 = "0.5" argon2 = "0.5"
askama = { version = "0.12", features = ["with-axum"] } askama = { version = "0.12", features = ["with-axum"] }
askama_axum = "0.3" askama_axum = "0.4"
async-session = "3" async-session = "3"
axum = { version = "0.6", features = ["macros", "headers"] } async-trait = "0.1.74"
axum-htmx = "0.3" axum = { version = "0.7", features = ["macros"] }
axum-login = { git = "https://github.com/nebkor/axum-login", branch = "sqlx-0.7", features = ["sqlite"], default-features = false } axum-htmx = "0.5"
axum-macros = "0.3" axum-login = "0.10"
axum-macros = "0.4"
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] } chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
clap = { version = "4", features = ["derive", "env", "unicode", "suggestions", "usage"] } clap = { version = "4", features = ["derive", "env", "unicode", "suggestions", "usage"] }
julid-rs = "1" julid-rs = "1"
@ -25,13 +26,15 @@ password-hash = { version = "0.5", features = ["std", "getrandom"] }
rand = "0.8" rand = "0.8"
rand_distr = "0.4" rand_distr = "0.4"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio-rustls", "sqlite"] } sha256 = { version = "1.4.0", default-features = false }
sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio", "sqlite", "tls-none", "migrate"] }
thiserror = "1" thiserror = "1"
tokio = { version = "1", features = ["full", "tracing"], default-features = false } tokio = { version = "1", features = ["full", "tracing"], default-features = false }
tokio-retry = "0.3" tokio-retry = "0.3"
tokio-stream = "0.1" tokio-stream = "0.1"
tower = { version = "0.4", features = ["util", "timeout"], default-features = false } tower = { version = "0.4", features = ["util", "timeout"], default-features = false }
tower-http = { version = "0.4", features = ["add-extension", "trace", "tracing", "fs"], default-features = false } tower-http = { version = "0.4", features = ["add-extension", "trace", "tracing", "fs"], default-features = false }
tower-sessions = { version = "0.7", default-features = false, features = ["sqlite-store"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
unicode-segmentation = "1" unicode-segmentation = "1"

445
src/db.rs
View file

@ -1,24 +1,22 @@
use std::time::Duration;
use async_session::SessionStore; use async_session::SessionStore;
use axum_login::{ use axum_login::{AuthManagerLayer, AuthManagerLayerBuilder, AuthSession, AuthUser, AuthnBackend};
axum_sessions::{PersistencePolicy, SessionLayer},
AuthLayer, SqliteStore, SqlxStore,
};
use julid::Julid; use julid::Julid;
use session_store::SqliteSessionStore;
use sqlx::{ use sqlx::{
migrate::Migrator, migrate::Migrator,
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}, sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
SqlitePool, SqlitePool,
}; };
use tower_sessions::{
cookie::time::Duration, session_store::ExpiredDeletion, Expiry, Session, SessionManagerLayer,
SqliteStore,
};
use crate::User; use crate::User;
const MAX_CONNS: u32 = 200; const MAX_CONNS: u32 = 200;
const MIN_CONNS: u32 = 5; const MIN_CONNS: u32 = 5;
const TIMEOUT: u64 = 11; const TIMEOUT: u64 = 11;
const SESSION_TTL: Duration = Duration::from_secs((365.2422 * 24. * 3600.0) as u64); const SESSION_TTL: Duration = Duration::new((365.2422 * 24. * 3600.0) as i64, 0);
pub fn get_db_pool() -> SqlitePool { pub fn get_db_pool() -> SqlitePool {
let db_filename = { let db_filename = {
@ -88,34 +86,23 @@ pub fn get_db_pool() -> SqlitePool {
pool pool
} }
pub async fn session_layer(pool: SqlitePool, secret: &[u8]) -> SessionLayer<SqliteSessionStore> { pub async fn session_layer(pool: SqlitePool) -> SessionManagerLayer<SqliteStore> {
let store = session_store::SqliteSessionStore::from_client(pool); let store = SqliteStore::new(pool);
store store
.migrate() .migrate()
.await .await
.expect("Calling `migrate()` should be reliable, is the DB gone?"); .expect("Calling `migrate()` should be reliable, is the DB gone?");
// since the secret is new every time the server starts, old sessions won't be SessionManagerLayer::new(store)
// valid anymore; if there were ever more than one service host or there were
// managed secrets, this would need to go away.
store
.clear_store()
.await
.unwrap_or_else(|e| tracing::error!("Could not delete old sessions; got error: {e}"));
SessionLayer::new(store, secret)
.with_secure(true) .with_secure(true)
.with_session_ttl(Some(SESSION_TTL)) .with_expiry(Expiry::OnInactivity(SESSION_TTL.into()))
.with_persistence_policy(PersistencePolicy::ExistingOnly)
} }
pub async fn auth_layer( pub async fn auth_layer(
pool: SqlitePool, pool: SqlitePool,
secret: &[u8], secret: &[u8],
) -> AuthLayer<SqlxStore<SqlitePool, User>, Julid, User> { ) -> AuthManagerLayer<SqliteStore, SessionManagerLayer<SqliteStore>> {
const QUERY: &str = "select * from users where id = $1"; todo!()
let store = SqliteStore::<User>::new(pool).with_query(QUERY);
AuthLayer::new(store, secret)
} }
//-************************************************************************ //-************************************************************************
@ -137,411 +124,3 @@ mod tests {
}); });
} }
} }
//-************************************************************************
// End public interface.
//-************************************************************************
//-************************************************************************
// Session store sub-module, not a public lib.
//-************************************************************************
#[allow(dead_code)]
mod session_store {
use async_session::{async_trait, chrono::Utc, log, serde_json, Result, Session};
use sqlx::{pool::PoolConnection, Sqlite};
use super::*;
/*
NOTE! This code was straight stolen from
https://github.com/jbr/async-sqlx-session/blob/30d00bed44ab2034082698f098eba48b21600f36/src/sqlite.rs
and used under the terms of the MIT license:
Copyright 2022 Jacob Rothstein
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the Software), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/// sqlx sqlite session store for async-sessions
#[derive(Clone, Debug)]
pub struct SqliteSessionStore {
client: SqlitePool,
table_name: String,
}
impl SqliteSessionStore {
/// constructs a new SqliteSessionStore from an existing
/// sqlx::SqlitePool. the default table name for this session
/// store will be "async_sessions". To override this, chain this
/// with [`with_table_name`](crate::SqliteSessionStore::with_table_name).
pub fn from_client(client: SqlitePool) -> Self {
Self {
client,
table_name: "async_sessions".into(),
}
}
/// Constructs a new SqliteSessionStore from a sqlite: database url.
/// note that this documentation uses the special `:memory:`
/// sqlite database for convenient testing, but a real
/// application would use a path like
/// `sqlite:///path/to/database.db`. The default table name for
/// this session store will be "async_sessions". To
/// override this, either chain with
/// [`with_table_name`](crate::SqliteSessionStore::with_table_name) or
/// use
/// [`new_with_table_name`](crate::SqliteSessionStore::new_with_table_name)
pub async fn new(database_url: &str) -> sqlx::Result<Self> {
Ok(Self::from_client(SqlitePool::connect(database_url).await?))
}
/// constructs a new SqliteSessionStore from a sqlite: database url. the
/// default table name for this session store will be
/// "async_sessions". To override this, either chain with
/// [`with_table_name`](crate::SqliteSessionStore::with_table_name) or
/// use
/// [`new_with_table_name`](crate::SqliteSessionStore::new_with_table_name)
pub async fn new_with_table_name(
database_url: &str,
table_name: &str,
) -> sqlx::Result<Self> {
Ok(Self::new(database_url).await?.with_table_name(table_name))
}
/// Chainable method to add a custom table name. This will panic
/// if the table name is not `[a-zA-Z0-9_-]+`.
pub fn with_table_name(mut self, table_name: impl AsRef<str>) -> Self {
let table_name = table_name.as_ref();
if table_name.is_empty()
|| !table_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
panic!(
"table name must be [a-zA-Z0-9_-]+, but {} was not",
table_name
);
}
self.table_name = table_name.to_owned();
self
}
/// Creates a session table if it does not already exist. If it
/// does, this will noop, making it safe to call repeatedly on
/// store initialization. In the future, this may make
/// exactly-once modifications to the schema of the session table
/// on breaking releases.
pub async fn migrate(&self) -> sqlx::Result<()> {
log::info!("migrating sessions on `{}`", self.table_name);
sqlx::query(&self.substitute_table_name(
r#"
CREATE TABLE IF NOT EXISTS %%TABLE_NAME%% (
id TEXT PRIMARY KEY NOT NULL,
expires INTEGER NULL,
session TEXT NOT NULL
)
"#,
))
.execute(&self.client)
.await?;
Ok(())
}
// private utility function because sqlite does not support
// parametrized table names
fn substitute_table_name(&self, query: &str) -> String {
query.replace("%%TABLE_NAME%%", &self.table_name)
}
/// retrieve a connection from the pool
async fn connection(&self) -> sqlx::Result<PoolConnection<Sqlite>> {
self.client.acquire().await
}
/// Performs a one-time cleanup task that clears out stale
/// (expired) sessions. You may want to call this from cron.
pub async fn cleanup(&self) -> sqlx::Result<()> {
sqlx::query(&self.substitute_table_name(
r#"
DELETE FROM %%TABLE_NAME%%
WHERE expires < ?
"#,
))
.bind(Utc::now().timestamp())
.execute(&self.client)
.await?;
Ok(())
}
/// retrieves the number of sessions currently stored, including
/// expired sessions
pub async fn count(&self) -> sqlx::Result<i32> {
let (count,) =
sqlx::query_as(&self.substitute_table_name("SELECT COUNT(*) FROM %%TABLE_NAME%%"))
.fetch_one(&self.client)
.await?;
Ok(count)
}
}
#[async_trait]
impl SessionStore for SqliteSessionStore {
async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
let id = Session::id_from_cookie_value(&cookie_value)?;
let result: Option<(String,)> = sqlx::query_as(&self.substitute_table_name(
r#"
SELECT session FROM %%TABLE_NAME%%
WHERE id = ? AND (expires IS NULL OR expires > ?)
"#,
))
.bind(&id)
.bind(Utc::now().timestamp())
.fetch_optional(&self.client)
.await?;
Ok(result
.map(|(session,)| serde_json::from_str(&session))
.transpose()?)
}
async fn store_session(&self, session: Session) -> Result<Option<String>> {
let id = session.id();
let string = serde_json::to_string(&session)?;
sqlx::query(&self.substitute_table_name(
r#"
INSERT INTO %%TABLE_NAME%%
(id, session, expires) VALUES (?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
expires = excluded.expires,
session = excluded.session
"#,
))
.bind(id)
.bind(&string)
.bind(session.expiry().map(|expiry| expiry.timestamp()))
.execute(&self.client)
.await?;
Ok(session.into_cookie_value())
}
async fn destroy_session(&self, session: Session) -> Result {
let id = session.id();
sqlx::query(&self.substitute_table_name(
r#"
DELETE FROM %%TABLE_NAME%% WHERE id = ?
"#,
))
.bind(id)
.execute(&self.client)
.await?;
Ok(())
}
async fn clear_store(&self) -> Result {
sqlx::query(&self.substitute_table_name(
r#"
DELETE FROM %%TABLE_NAME%%
"#,
))
.execute(&self.client)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
async fn test_store() -> SqliteSessionStore {
let store = SqliteSessionStore::new("sqlite::memory:")
.await
.expect("building a sqlite :memory: SqliteSessionStore");
store
.migrate()
.await
.expect("migrating a brand new :memory: SqliteSessionStore");
store
}
#[tokio::test]
async fn creating_a_new_session_with_no_expiry() -> Result {
let store = test_store().await;
let mut session = Session::new();
session.insert("key", "value")?;
let cloned = session.clone();
let cookie_value = store.store_session(session).await?.unwrap();
let (id, expires, serialized, count): (String, Option<i64>, String, i64) =
sqlx::query_as("select id, expires, session, count(*) from async_sessions")
.fetch_one(&store.client)
.await?;
assert_eq!(1, count);
assert_eq!(id, cloned.id());
assert_eq!(expires, None);
let deserialized_session: Session = serde_json::from_str(&serialized)?;
assert_eq!(cloned.id(), deserialized_session.id());
assert_eq!("value", &deserialized_session.get::<String>("key").unwrap());
let loaded_session = store.load_session(cookie_value).await?.unwrap();
assert_eq!(cloned.id(), loaded_session.id());
assert_eq!("value", &loaded_session.get::<String>("key").unwrap());
assert!(!loaded_session.is_expired());
Ok(())
}
#[tokio::test]
async fn updating_a_session() -> Result {
let store = test_store().await;
let mut session = Session::new();
let original_id = session.id().to_owned();
session.insert("key", "value")?;
let cookie_value = store.store_session(session).await?.unwrap();
let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
session.insert("key", "other value")?;
assert_eq!(None, store.store_session(session).await?);
let session = store.load_session(cookie_value.clone()).await?.unwrap();
assert_eq!(session.get::<String>("key").unwrap(), "other value");
let (id, count): (String, i64) =
sqlx::query_as("select id, count(*) from async_sessions")
.fetch_one(&store.client)
.await?;
assert_eq!(1, count);
assert_eq!(original_id, id);
Ok(())
}
#[tokio::test]
async fn updating_a_session_extending_expiry() -> Result {
let store = test_store().await;
let mut session = Session::new();
session.expire_in(Duration::from_secs(10));
let original_id = session.id().to_owned();
let original_expires = *session.expiry().unwrap();
let cookie_value = store.store_session(session).await?.unwrap();
let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
assert_eq!(session.expiry().unwrap(), &original_expires);
session.expire_in(Duration::from_secs(20));
let new_expires = *session.expiry().unwrap();
store.store_session(session).await?;
let session = store.load_session(cookie_value.clone()).await?.unwrap();
assert_eq!(session.expiry().unwrap(), &new_expires);
let (id, expires, count): (String, i64, i64) =
sqlx::query_as("select id, expires, count(*) from async_sessions")
.fetch_one(&store.client)
.await?;
assert_eq!(1, count);
assert_eq!(expires, new_expires.timestamp());
assert_eq!(original_id, id);
Ok(())
}
#[tokio::test]
async fn creating_a_new_session_with_expiry() -> Result {
let store = test_store().await;
let mut session = Session::new();
session.expire_in(Duration::from_secs(1));
session.insert("key", "value")?;
let cloned = session.clone();
let cookie_value = store.store_session(session).await?.unwrap();
let (id, expires, serialized, count): (String, Option<i64>, String, i64) =
sqlx::query_as("select id, expires, session, count(*) from async_sessions")
.fetch_one(&store.client)
.await?;
assert_eq!(1, count);
assert_eq!(id, cloned.id());
assert!(expires.unwrap() > Utc::now().timestamp());
let deserialized_session: Session = serde_json::from_str(&serialized)?;
assert_eq!(cloned.id(), deserialized_session.id());
assert_eq!("value", &deserialized_session.get::<String>("key").unwrap());
let loaded_session = store.load_session(cookie_value.clone()).await?.unwrap();
assert_eq!(cloned.id(), loaded_session.id());
assert_eq!("value", &loaded_session.get::<String>("key").unwrap());
assert!(!loaded_session.is_expired());
tokio::time::sleep(Duration::from_secs(1)).await;
assert_eq!(None, store.load_session(cookie_value).await?);
Ok(())
}
#[tokio::test]
async fn destroying_a_single_session() -> Result {
let store = test_store().await;
for _ in 0..3i8 {
store.store_session(Session::new()).await?;
}
let cookie = store.store_session(Session::new()).await?.unwrap();
assert_eq!(4, store.count().await?);
let session = store.load_session(cookie.clone()).await?.unwrap();
store.destroy_session(session.clone()).await.unwrap();
assert_eq!(None, store.load_session(cookie).await?);
assert_eq!(3, store.count().await?);
// // attempting to destroy the session again is not an error
// assert!(store.destroy_session(session).await.is_ok());
Ok(())
}
#[tokio::test]
async fn clearing_the_whole_store() -> Result {
let store = test_store().await;
for _ in 0..3i8 {
store.store_session(Session::new()).await?;
}
assert_eq!(3, store.count().await?);
store.clear_store().await.unwrap();
assert_eq!(0, store.count().await?);
Ok(())
}
}
}

View file

@ -3,8 +3,12 @@ use std::{
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use axum::{extract::State, http::Request, middleware::Next, response::IntoResponse}; use axum::{
use axum_login::{secrecy::SecretVec, AuthUser}; extract::{Request, State},
middleware::Next,
response::IntoResponse,
};
use axum_login::AuthUser;
use julid::Julid; use julid::Julid;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::SqlitePool; use sqlx::SqlitePool;
@ -49,13 +53,15 @@ impl Display for User {
} }
} }
impl AuthUser<Julid> for User { impl AuthUser for User {
fn get_id(&self) -> Julid { type Id = Julid;
fn id(&self) -> Self::Id {
self.id self.id
} }
fn get_password_hash(&self) -> SecretVec<u8> { fn session_auth_hash(&self) -> &[u8] {
SecretVec::new(self.pwhash.as_bytes().to_vec()) self.pwhash.as_bytes()
} }
} }
@ -86,11 +92,11 @@ impl User {
// User-specific middleware // User-specific middleware
//-************************************************************************ //-************************************************************************
pub async fn handle_update_last_seen<BodyT>( pub async fn handle_update_last_seen(
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
auth: AuthContext, auth: AuthContext,
request: Request<BodyT>, request: Request,
next: Next<BodyT>, next: Next,
) -> impl IntoResponse { ) -> impl IntoResponse {
if let Some(user) = auth.current_user { if let Some(user) = auth.current_user {
if let Some(then) = user.last_seen { if let Some(then) = user.last_seen {