feat: got sessions working

This commit is contained in:
Andrew Rioux
2025-01-29 18:39:10 -05:00
parent bf879bb081
commit 0d6b2b4c16
9 changed files with 339 additions and 431 deletions
+145 -73
View File
@@ -1,3 +1,11 @@
use leptos::{prelude::expect_context, server_fn::error::NoCustomError};
use leptos_axum::{extract, ResponseOptions};
use leptos::prelude::ServerFnError;
use pbkdf2::{Pbkdf2, password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, rand_core::{OsRng, RngCore}, SaltString}};
use sqlx::SqlitePool;
use crate::error::Error;
#[derive(Clone)]
pub struct User {
pub user_id: i64,
@@ -6,13 +14,6 @@ pub struct User {
pub last_active: Option<i64>
}
use async_trait::async_trait;
use pbkdf2::{Pbkdf2, password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, rand_core::OsRng, SaltString}};
use axum_login::{AuthUser, AuthnBackend, UserId};
use sqlx::SqlitePool;
use crate::error::Error;
impl std::fmt::Debug for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("User")
@@ -23,27 +24,16 @@ impl std::fmt::Debug for User {
}
}
impl AuthUser for User {
type Id = i64;
fn id(&self) -> Self::Id {
self.user_id
}
fn session_auth_hash(&self) -> &[u8] {
self.password_hash.as_bytes()
}
}
async fn hash_password(pass: &[u8]) -> Result<String, Error> {
Ok(tokio::task::spawn_blocking({
let pass = pass.to_owned();
let salt = SaltString::generate(&mut OsRng);
move || Pbkdf2.hash_password(
&*pass,
&salt,
).map(|hash| hash.to_string())
move ||
Pbkdf2.hash_password(
&*pass,
&salt,
).map(|hash| hash.serialize().as_str().to_string())
}).await??)
}
@@ -114,67 +104,149 @@ where
Ok(())
}
#[derive(Clone)]
pub struct Backend(SqlitePool);
const SESSION_ID_KEY: &'static str = "session_id";
const SESSION_AGE: i64 = 30 * 60;
impl Backend {
pub fn new(db: SqlitePool) -> Self {
Self(db)
}
}
pub async fn create_auth_session(username: String, password: String) -> Result<(), ServerFnError> {
use axum_extra::extract::cookie::{Cookie, SameSite};
use axum::http::{header, HeaderValue};
#[async_trait]
impl AuthnBackend for Backend {
type User = User;
type Credentials = (String, String);
type Error = Error;
let db = expect_context::<SqlitePool>();
let resp = expect_context::<ResponseOptions>();
async fn authenticate(
&self,
creds: Self::Credentials
) -> Result<Option<Self::User>, Self::Error> {
let user: Option<Self::User> = sqlx::query_as!(
User,
"SELECT * FROM users WHERE user_name = ?",
creds.0
let user: Option<User> = sqlx::query_as!(
User,
"SELECT * FROM users WHERE user_name = ?",
username
)
.fetch_optional(&db)
.await?;
let Some(user) = user else {
return Err(ServerFnError::<NoCustomError>::ServerError("Invalid credentials".to_string()));
};
let good_hash = verify_password(
&password,
&user.password_hash
).await?;
if good_hash {
let now = chrono::Utc::now().timestamp();
let expires = now + SESSION_AGE;
sqlx::query!(
"UPDATE users SET last_active = ?",
now
)
.fetch_optional(&self.0)
.execute(&db)
.await?;
let Some(user) = user else { return Ok(None); };
let session_id: String = tokio::task::spawn_blocking(|| {
let mut key = [0u8; 32];
OsRng.fill_bytes(&mut key);
hex::encode(&key[..])
}).await?;
let good_hash = verify_password(
&user.password_hash,
&creds.1
).await?;
sqlx::query!(
"INSERT INTO sessions (session_id, user_id, expires) VALUES (?, ?, ?)",
session_id,
user.user_id,
expires
)
.execute(&db)
.await?;
if good_hash {
let now = chrono::Utc::now().timestamp();
let cookie = Cookie::build((SESSION_ID_KEY, &session_id))
.http_only(true)
.path("/")
.same_site(SameSite::Lax);
sqlx::query!(
"UPDATE users SET last_active = ?",
now
)
.execute(&self.0)
.await?;
Ok(Some(user))
} else {
Ok(None)
if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) {
resp.insert_header(header::SET_COOKIE, cookie);
}
}
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
let user: Option<Self::User> = sqlx::query_as!(
User,
"SELECT * FROM users WHERE user_id = ?",
user_id
)
.fetch_optional(&self.0)
.await?;
Ok(user)
Ok(())
} else {
Err(ServerFnError::<NoCustomError>::ServerError("Invalid credentials".to_string()))
}
}
pub type AuthSession = axum_login::AuthSession<Backend>;
pub async fn destroy_auth_session() -> Result<(), ServerFnError> {
use axum_extra::extract::cookie::CookieJar;
let db = expect_context::<SqlitePool>();
let jar = extract::<CookieJar>().await?;
let Some(cookie) = jar.get(SESSION_ID_KEY) else {
return Ok(());
};
let session_id = cookie.value();
sqlx::query!(
"DELETE FROM sessions WHERE session_id = ?",
session_id
)
.execute(&db)
.await?;
Ok(())
}
pub async fn get_auth_session() -> Result<Option<User>, ServerFnError> {
use axum_extra::extract::cookie::CookieJar;
let db = expect_context::<SqlitePool>();
let jar = extract::<CookieJar>().await?;
let Some(cookie) = jar.get(SESSION_ID_KEY) else {
return Ok(None);
};
let now = chrono::Utc::now().timestamp();
let session_id = cookie.value();
let user = sqlx::query_as!(
User,
"SELECT users.user_id, user_name, password_hash, last_active \
FROM users \
INNER JOIN sessions \
WHERE session_id = ? \
AND expires > ?",
session_id,
now
)
.fetch_optional(&db)
.await?;
if let Some(u) = &user {
let now = chrono::Utc::now().timestamp();
let expires = now + SESSION_AGE;
sqlx::query!(
"UPDATE users SET last_active = ? WHERE user_id = ?",
now,
u.user_id
)
.execute(&db)
.await?;
sqlx::query!(
"UPDATE sessions SET expires = ? WHERE session_id = ?",
expires,
session_id
)
.execute(&db)
.await?;
}
sqlx::query!(
"DELETE FROM sessions WHERE expires < ?",
now
)
.execute(&db)
.await?;
Ok(user)
}