what2watch/src/users.rs
2023-06-20 16:32:36 -07:00

110 lines
3.3 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 serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use crate::{AuthContext, DbId};
const USERNAME_QUERY: &str = "select * from witches where username = $1";
const LAST_SEEN_QUERY: &str = "update witches set last_seen = (select unixepoch()) where id = $1";
#[derive(Default, Clone, PartialEq, Eq, sqlx::FromRow, Serialize, Deserialize)]
pub struct User {
pub id: DbId,
pub username: String,
pub displayname: Option<String>,
pub email: Option<String>,
pub last_seen: Option<i64>,
pub(crate) pwhash: String,
}
impl Debug for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("User")
.field("id", &self.id)
.field("username", &self.username)
.field("displayname", &self.displayname)
.field("email", &self.email)
.field("last_seen", &self.last_seen)
.field("pwhash", &"<redacted>")
.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<DbId> for User {
fn get_id(&self) -> DbId {
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.0.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
}