From 97623afd8d11d29f7c4b9f0f4d0572c1674a360d Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Sun, 14 Apr 2024 15:22:26 -0700 Subject: [PATCH] fix date handling. --- Cargo.lock | 5 +++ Cargo.toml | 4 +- migrations/20240409233522_views.down.sql | 1 + migrations/20240409233522_views.up.sql | 5 ++- src/auth.rs | 7 ++-- src/conf.rs | 8 +++- src/db.rs | 7 +++- src/lib.rs | 4 ++ src/main.rs | 2 +- src/signup/handlers.rs | 2 +- src/signup/mod.rs | 7 +++- src/users.rs | 49 +++++++++++------------- 12 files changed, 61 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 469b2b7..1ece25f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,6 +444,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", + "serde", "time 0.1.45", "windows-targets 0.48.5", ] @@ -2040,6 +2041,7 @@ dependencies = [ "atoi", "byteorder", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -2120,6 +2122,7 @@ dependencies = [ "bitflags 2.5.0", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -2162,6 +2165,7 @@ dependencies = [ "base64 0.21.7", "bitflags 2.5.0", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -2198,6 +2202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 4999985..cfdaa01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ async-trait = "0.1" axum = { version = "0.7", features = ["macros"] } axum-login = "0.14" axum-macros = "0.4" -chrono = { version = "0.4", default-features = false, features = ["std", "clock"] } +chrono = { version = "0.4", default-features = false, features = ["std", "clock", "serde"] } clap = { version = "4", features = ["derive", "env", "unicode", "suggestions", "usage"] } confy = "0.6" dirs = "5" @@ -29,7 +29,7 @@ password-hash = { version = "0.5", features = ["std", "getrandom"] } rand = "0.8" serde = { version = "1", features = ["derive"] } sha256 = { version = "1", default-features = false } -sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio", "sqlite", "tls-none", "migrate"] } +sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio", "sqlite", "tls-none", "migrate", "chrono"] } thiserror = "1" tokio = { version = "1", features = ["rt-multi-thread", "signal", "tracing"], default-features = false } tower = { version = "0.4", features = ["util", "timeout"], default-features = false } diff --git a/migrations/20240409233522_views.down.sql b/migrations/20240409233522_views.down.sql index c77f738..74856cd 100644 --- a/migrations/20240409233522_views.down.sql +++ b/migrations/20240409233522_views.down.sql @@ -1,3 +1,4 @@ +drop view if exists q; drop view if exists i; drop view if exists u; drop view if exists s; diff --git a/migrations/20240409233522_views.up.sql b/migrations/20240409233522_views.up.sql index 327035b..62539c4 100644 --- a/migrations/20240409233522_views.up.sql +++ b/migrations/20240409233522_views.up.sql @@ -1,5 +1,6 @@ +-- human-friendly views with joined fields and string julids create view if not exists w as select julid_string(id) id, kind, title, metadata_url, length, release_date, last_updated from watches; create view if not exists s as select julid_string(id) id, name, born, died from stars; -create view if not exists u as select julid_string(id) id, username, displayname, email, last_seen, last_updated from users; +create view if not exists u as select julid_string(id) id, username, displayname, email, (select username from users where id = invited_by) invited_by, last_seen, last_updated from users; create view if not exists i as select julid_string(invites.id) id, users.username, expires_at, remaining, invites.last_updated from invites inner join users on users.id = owner; -create view if not exists q as select users.username, watches.title from watch_quests inner join users on users.id = user inner join watches on watch = watches.id; +create view if not exists q as select users.username, watches.title, julid_string(watch) from watch_quests inner join users on users.id = user inner join watches on watch = watches.id; diff --git a/src/auth.rs b/src/auth.rs index 2c0c96f..fce998e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -50,9 +50,10 @@ impl AuthnBackend for AuthStore { let username = creds.username.trim(); let password = creds.password.trim(); - let user = User::try_get(username, &self.0) - .await - .map_err(|_| AuthErrorKind::Internal)?; + let user = User::try_get(username, &self.0).await.map_err(|e| { + tracing::debug!("Got error getting {username}: {e:?}"); + AuthErrorKind::Internal + })?; Ok(user.filter(|user| verify_password(password, &user.pwhash).is_ok())) } diff --git a/src/conf.rs b/src/conf.rs index 1fa17bb..88761ac 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -7,6 +7,7 @@ const CONFIG_NAME: Option<&str> = Some("config"); pub struct Config { pub base_url: String, pub db_file: String, + pub julid_plugin: String, } impl Default for Config { @@ -14,9 +15,14 @@ impl Default for Config { let mut datadir = dirs::data_dir().unwrap(); datadir.push(APP_NAME); datadir.push("what2watch.db"); + let db_file = datadir.as_os_str().to_string_lossy().to_string(); + datadir.pop(); + datadir.push("libjulid"); // don't have the '.so' extension here + let julid_plugin = datadir.as_os_str().to_string_lossy().to_string(); Self { base_url: "http://localhost:3000".into(), - db_file: datadir.as_os_str().to_string_lossy().to_string(), + db_file, + julid_plugin, } } } diff --git a/src/db.rs b/src/db.rs index 198bdd3..cc17c75 100644 --- a/src/db.rs +++ b/src/db.rs @@ -11,11 +11,12 @@ const MIN_CONNS: u32 = 5; const TIMEOUT: u64 = 20; pub fn get_db_pool() -> SqlitePool { + let conf = crate::conf::Config::get(); let db_filename = { std::env::var("DATABASE_FILE").unwrap_or_else(|_| { #[cfg(not(test))] { - let f = crate::conf::Config::get().db_file; + let f = conf.db_file; let p = std::path::Path::new(&f); let p = p.parent().unwrap(); std::fs::create_dir_all(p).expect("couldn't create data dir"); @@ -36,13 +37,15 @@ pub fn get_db_pool() -> SqlitePool { tracing::info!("Connecting to DB at {db_filename}"); + let plugin = conf.julid_plugin; + let conn_opts = SqliteConnectOptions::new() .foreign_keys(true) .journal_mode(SqliteJournalMode::Wal) .synchronous(sqlx::sqlite::SqliteSynchronous::Normal) .filename(&db_filename) // be sure to have run `make` so that the libjulid extension is built - .extension("./libjulid") + .extension(plugin) .busy_timeout(Duration::from_secs(TIMEOUT)) .create_if_missing(true) .optimize_on_close(true, None) diff --git a/src/lib.rs b/src/lib.rs index 8331f59..ac0e3af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ pub use users::User; pub use watches::{ShowKind, Watch, WatchQuest}; pub type WWRouter = axum::Router; +pub type WatchDate = chrono::DateTime; // everything else is private to the crate mod auth; @@ -55,6 +56,9 @@ pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService { post_add_watch_quest, }; + let conf = crate::conf::Config::get(); + tracing::info!("Using config: {conf:#?}"); + let auth_layer = { let session_layer = session_layer(db_pool.clone()).await; let store = AuthStore::new(db_pool.clone()); diff --git a/src/main.rs b/src/main.rs index 42be8b3..736a7eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "what2watch=debug,axum::routing=debug".into()), + .unwrap_or_else(|_| "what2watch=debug,axum=debug".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); diff --git a/src/signup/handlers.rs b/src/signup/handlers.rs index c34f7bd..112dc7a 100644 --- a/src/signup/handlers.rs +++ b/src/signup/handlers.rs @@ -250,7 +250,7 @@ async fn validate_invitation( } if let Some(ts) = invitation.expires_at { - let now = chrono::Utc::now().timestamp(); + let now = chrono::Utc::now(); if ts < now { return Err(CreateUserErrorKind::BadInvitation); } diff --git a/src/signup/mod.rs b/src/signup/mod.rs index 72d5264..e07e1b5 100644 --- a/src/signup/mod.rs +++ b/src/signup/mod.rs @@ -4,6 +4,8 @@ use julid::Julid; use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; +use crate::WatchDate; + pub mod handlers; pub mod templates; @@ -25,7 +27,7 @@ pub enum CreateInviteErrorKind { pub struct Invitation { id: Julid, owner: Julid, - expires_at: Option, + expires_at: Option, remaining: i16, } @@ -85,8 +87,9 @@ impl Invitation { } pub fn with_expires_in(&self, expires_in: Duration) -> Self { + let now = chrono::Utc::now(); Self { - expires_at: Some((chrono::Utc::now() + expires_in).timestamp()), + expires_at: Some(now + expires_in), ..*self } } diff --git a/src/users.rs b/src/users.rs index 0daeb45..822d829 100644 --- a/src/users.rs +++ b/src/users.rs @@ -9,12 +9,7 @@ use julid::Julid; use serde::{Deserialize, Serialize}; use sqlx::{sqlite::SqliteRow, Row, SqlitePool}; -use crate::AuthSession; - -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"; -const INSERT_QUERY: &str = - "insert into users (id, username, displayname, email, pwhash, invited_by) values ($1, $2, $3, $4, $5, $6)"; +use crate::{AuthSession, WatchDate}; #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct User { @@ -22,7 +17,7 @@ pub struct User { pub username: String, pub displayname: Option, pub email: Option, - pub last_seen: Option, + pub last_seen: Option, pub pwhash: String, pub invited_by: Julid, pub is_active: bool, @@ -89,30 +84,35 @@ impl Display for User { impl User { pub async fn try_get(username: &str, db: &SqlitePool) -> Result, sqlx::Error> { - sqlx::query_as(USERNAME_QUERY) + sqlx::query_as("select * from users where username = ?") .bind(username) .fetch_optional(db) .await } + /// This is mostly for tests and to ensure that the system accounts are + /// present. Most of the time, users should not be inserted with an ID, but + /// should let the DB assign them an ID. pub async fn try_insert(&self, db: &SqlitePool) -> Result<(), sqlx::Error> { - sqlx::query(INSERT_QUERY) - .bind(self.id) - .bind(&self.username) - .bind(&self.displayname) - .bind(&self.email) - .bind(&self.pwhash) - .bind(self.invited_by) + sqlx::query!("insert into users (id, username, displayname, email, pwhash, invited_by) values (?, ?, ?, ?, ?, ?)", + self.id, + self.username, + self.displayname, + self.email, + self.pwhash, + self.invited_by) .execute(db) .await .map(|_| ()) } pub async fn update_last_seen(&self, pool: &SqlitePool) { - match sqlx::query(LAST_SEEN_QUERY) - .bind(self.id) - .execute(pool) - .await + match sqlx::query!( + "update users set last_seen = CURRENT_TIMESTAMP where id = ?", + self.id + ) + .execute(pool) + .await { Ok(_) => {} Err(e) => { @@ -143,15 +143,12 @@ pub async fn handle_update_last_seen( request: Request, next: Next, ) -> impl IntoResponse { - use std::time::{SystemTime, UNIX_EPOCH}; if let Some(user) = auth.user { - if let Some(then) = user.last_seen { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64; + if let Some(then) = &user.last_seen { + let now = chrono::Utc::now(); // The Nyquist frequency for 1-day tracking resolution is 12 hours. - if now - then > 12 * 3600 { + let dur = chrono::Duration::hours(12); + if (now - then) > dur { user.update_last_seen(&pool).await; } } else {