what2watch/src/users.rs
2023-07-29 12:27:12 -07:00

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
}