use std::{ fmt::{Debug, Display}, time::{SystemTime, UNIX_EPOCH}, }; use axum::{extract::State, http::Request, middleware::Next, response::IntoResponse}; use axum_login::{secrecy::SecretVec, AuthUser}; use julid::Julid; use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; use crate::AuthContext; const USERNAME_QUERY: &str = "select * from users where username = $1"; const LAST_SEEN_QUERY: &str = "update users set last_seen = (select unixepoch()) where id = $1"; #[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] pub struct User { pub id: Julid, pub username: String, pub displayname: Option, pub email: Option, pub last_seen: Option, pub pwhash: String, } impl Debug for User { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("User") .field("username", &self.username) .field("id", &self.id.as_string()) .field("displayname", &self.displayname) .field("email", &self.email) .field("last_seen", &self.last_seen) .finish() } } impl Display for User { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let uname = &self.username; let dname = if let Some(ref n) = self.displayname { n } else { "" }; let email = if let Some(ref e) = self.email { e } else { "" }; write!(f, "Username: {uname}\nDisplayname: {dname}\nEmail: {email}") } } impl AuthUser for User { fn get_id(&self) -> Julid { self.id } fn get_password_hash(&self) -> SecretVec { SecretVec::new(self.pwhash.as_bytes().to_vec()) } } impl User { pub async fn try_get(username: &str, db: &SqlitePool) -> Result { sqlx::query_as(USERNAME_QUERY) .bind(username) .fetch_one(db) .await } pub async fn update_last_seen(&self, pool: &SqlitePool) { match sqlx::query(LAST_SEEN_QUERY) .bind(self.id) .execute(pool) .await { Ok(_) => {} Err(e) => { let id = self.id.to_string(); tracing::error!("Could not update last_seen for user {id}; got {e:?}"); } } } } //-************************************************************************ // User-specific middleware //-************************************************************************ pub async fn handle_update_last_seen( State(pool): State, auth: AuthContext, request: Request, next: Next, ) -> impl IntoResponse { if let Some(user) = auth.current_user { if let Some(then) = user.last_seen { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() as i64; // The Nyquist frequency for 1-day tracking resolution is 12 hours. if now - then > 12 * 3600 { user.update_last_seen(&pool).await; } } else { user.update_last_seen(&pool).await; } } next.run(request).await }