110 lines
3.2 KiB
Rust
110 lines
3.2 KiB
Rust
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<String>,
|
|
pub email: Option<String>,
|
|
pub last_seen: Option<i64>,
|
|
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<Julid> for User {
|
|
fn get_id(&self) -> Julid {
|
|
self.id
|
|
}
|
|
|
|
fn get_password_hash(&self) -> SecretVec<u8> {
|
|
SecretVec::new(self.pwhash.as_bytes().to_vec())
|
|
}
|
|
}
|
|
|
|
impl User {
|
|
pub async fn try_get(username: &str, db: &SqlitePool) -> Result<User, impl std::error::Error> {
|
|
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<BodyT>(
|
|
State(pool): State<SqlitePool>,
|
|
auth: AuthContext,
|
|
request: Request<BodyT>,
|
|
next: Next<BodyT>,
|
|
) -> 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
|
|
}
|