fix date handling.

This commit is contained in:
Joe Ardent 2024-04-14 15:22:26 -07:00
parent 4d8706d6bf
commit 97623afd8d
12 changed files with 61 additions and 40 deletions

5
Cargo.lock generated
View file

@ -444,6 +444,7 @@ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"num-traits", "num-traits",
"serde",
"time 0.1.45", "time 0.1.45",
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
@ -2040,6 +2041,7 @@ dependencies = [
"atoi", "atoi",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"crc", "crc",
"crossbeam-queue", "crossbeam-queue",
"either", "either",
@ -2120,6 +2122,7 @@ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"crc", "crc",
"digest", "digest",
"dotenvy", "dotenvy",
@ -2162,6 +2165,7 @@ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"bitflags 2.5.0", "bitflags 2.5.0",
"byteorder", "byteorder",
"chrono",
"crc", "crc",
"dotenvy", "dotenvy",
"etcetera", "etcetera",
@ -2198,6 +2202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
dependencies = [ dependencies = [
"atoi", "atoi",
"chrono",
"flume", "flume",
"futures-channel", "futures-channel",
"futures-core", "futures-core",

View file

@ -16,7 +16,7 @@ async-trait = "0.1"
axum = { version = "0.7", features = ["macros"] } axum = { version = "0.7", features = ["macros"] }
axum-login = "0.14" axum-login = "0.14"
axum-macros = "0.4" 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"] } clap = { version = "4", features = ["derive", "env", "unicode", "suggestions", "usage"] }
confy = "0.6" confy = "0.6"
dirs = "5" dirs = "5"
@ -29,7 +29,7 @@ password-hash = { version = "0.5", features = ["std", "getrandom"] }
rand = "0.8" rand = "0.8"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
sha256 = { version = "1", default-features = false } 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" thiserror = "1"
tokio = { version = "1", features = ["rt-multi-thread", "signal", "tracing"], default-features = false } tokio = { version = "1", features = ["rt-multi-thread", "signal", "tracing"], default-features = false }
tower = { version = "0.4", features = ["util", "timeout"], default-features = false } tower = { version = "0.4", features = ["util", "timeout"], default-features = false }

View file

@ -1,3 +1,4 @@
drop view if exists q;
drop view if exists i; drop view if exists i;
drop view if exists u; drop view if exists u;
drop view if exists s; drop view if exists s;

View file

@ -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 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 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 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;

View file

@ -50,9 +50,10 @@ impl AuthnBackend for AuthStore {
let username = creds.username.trim(); let username = creds.username.trim();
let password = creds.password.trim(); let password = creds.password.trim();
let user = User::try_get(username, &self.0) let user = User::try_get(username, &self.0).await.map_err(|e| {
.await tracing::debug!("Got error getting {username}: {e:?}");
.map_err(|_| AuthErrorKind::Internal)?; AuthErrorKind::Internal
})?;
Ok(user.filter(|user| verify_password(password, &user.pwhash).is_ok())) Ok(user.filter(|user| verify_password(password, &user.pwhash).is_ok()))
} }

View file

@ -7,6 +7,7 @@ const CONFIG_NAME: Option<&str> = Some("config");
pub struct Config { pub struct Config {
pub base_url: String, pub base_url: String,
pub db_file: String, pub db_file: String,
pub julid_plugin: String,
} }
impl Default for Config { impl Default for Config {
@ -14,9 +15,14 @@ impl Default for Config {
let mut datadir = dirs::data_dir().unwrap(); let mut datadir = dirs::data_dir().unwrap();
datadir.push(APP_NAME); datadir.push(APP_NAME);
datadir.push("what2watch.db"); 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 { Self {
base_url: "http://localhost:3000".into(), base_url: "http://localhost:3000".into(),
db_file: datadir.as_os_str().to_string_lossy().to_string(), db_file,
julid_plugin,
} }
} }
} }

View file

@ -11,11 +11,12 @@ const MIN_CONNS: u32 = 5;
const TIMEOUT: u64 = 20; const TIMEOUT: u64 = 20;
pub fn get_db_pool() -> SqlitePool { pub fn get_db_pool() -> SqlitePool {
let conf = crate::conf::Config::get();
let db_filename = { let db_filename = {
std::env::var("DATABASE_FILE").unwrap_or_else(|_| { std::env::var("DATABASE_FILE").unwrap_or_else(|_| {
#[cfg(not(test))] #[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 = std::path::Path::new(&f);
let p = p.parent().unwrap(); let p = p.parent().unwrap();
std::fs::create_dir_all(p).expect("couldn't create data dir"); 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}"); tracing::info!("Connecting to DB at {db_filename}");
let plugin = conf.julid_plugin;
let conn_opts = SqliteConnectOptions::new() let conn_opts = SqliteConnectOptions::new()
.foreign_keys(true) .foreign_keys(true)
.journal_mode(SqliteJournalMode::Wal) .journal_mode(SqliteJournalMode::Wal)
.synchronous(sqlx::sqlite::SqliteSynchronous::Normal) .synchronous(sqlx::sqlite::SqliteSynchronous::Normal)
.filename(&db_filename) .filename(&db_filename)
// be sure to have run `make` so that the libjulid extension is built // be sure to have run `make` so that the libjulid extension is built
.extension("./libjulid") .extension(plugin)
.busy_timeout(Duration::from_secs(TIMEOUT)) .busy_timeout(Duration::from_secs(TIMEOUT))
.create_if_missing(true) .create_if_missing(true)
.optimize_on_close(true, None) .optimize_on_close(true, None)

View file

@ -22,6 +22,7 @@ pub use users::User;
pub use watches::{ShowKind, Watch, WatchQuest}; pub use watches::{ShowKind, Watch, WatchQuest};
pub type WWRouter = axum::Router<SqlitePool>; pub type WWRouter = axum::Router<SqlitePool>;
pub type WatchDate = chrono::DateTime<chrono::Utc>;
// everything else is private to the crate // everything else is private to the crate
mod auth; mod auth;
@ -55,6 +56,9 @@ pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService<axum::Router> {
post_add_watch_quest, post_add_watch_quest,
}; };
let conf = crate::conf::Config::get();
tracing::info!("Using config: {conf:#?}");
let auth_layer = { let auth_layer = {
let session_layer = session_layer(db_pool.clone()).await; let session_layer = session_layer(db_pool.clone()).await;
let store = AuthStore::new(db_pool.clone()); let store = AuthStore::new(db_pool.clone());

View file

@ -7,7 +7,7 @@ fn main() {
tracing_subscriber::registry() tracing_subscriber::registry()
.with( .with(
tracing_subscriber::EnvFilter::try_from_default_env() 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()) .with(tracing_subscriber::fmt::layer())
.init(); .init();

View file

@ -250,7 +250,7 @@ async fn validate_invitation(
} }
if let Some(ts) = invitation.expires_at { if let Some(ts) = invitation.expires_at {
let now = chrono::Utc::now().timestamp(); let now = chrono::Utc::now();
if ts < now { if ts < now {
return Err(CreateUserErrorKind::BadInvitation); return Err(CreateUserErrorKind::BadInvitation);
} }

View file

@ -4,6 +4,8 @@ use julid::Julid;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::WatchDate;
pub mod handlers; pub mod handlers;
pub mod templates; pub mod templates;
@ -25,7 +27,7 @@ pub enum CreateInviteErrorKind {
pub struct Invitation { pub struct Invitation {
id: Julid, id: Julid,
owner: Julid, owner: Julid,
expires_at: Option<i64>, expires_at: Option<WatchDate>,
remaining: i16, remaining: i16,
} }
@ -85,8 +87,9 @@ impl Invitation {
} }
pub fn with_expires_in(&self, expires_in: Duration) -> Self { pub fn with_expires_in(&self, expires_in: Duration) -> Self {
let now = chrono::Utc::now();
Self { Self {
expires_at: Some((chrono::Utc::now() + expires_in).timestamp()), expires_at: Some(now + expires_in),
..*self ..*self
} }
} }

View file

@ -9,12 +9,7 @@ use julid::Julid;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{sqlite::SqliteRow, Row, SqlitePool}; use sqlx::{sqlite::SqliteRow, Row, SqlitePool};
use crate::AuthSession; use crate::{AuthSession, WatchDate};
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)";
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct User { pub struct User {
@ -22,7 +17,7 @@ pub struct User {
pub username: String, pub username: String,
pub displayname: Option<String>, pub displayname: Option<String>,
pub email: Option<String>, pub email: Option<String>,
pub last_seen: Option<i64>, pub last_seen: Option<WatchDate>,
pub pwhash: String, pub pwhash: String,
pub invited_by: Julid, pub invited_by: Julid,
pub is_active: bool, pub is_active: bool,
@ -89,30 +84,35 @@ impl Display for User {
impl User { impl User {
pub async fn try_get(username: &str, db: &SqlitePool) -> Result<Option<Self>, sqlx::Error> { pub async fn try_get(username: &str, db: &SqlitePool) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as(USERNAME_QUERY) sqlx::query_as("select * from users where username = ?")
.bind(username) .bind(username)
.fetch_optional(db) .fetch_optional(db)
.await .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> { pub async fn try_insert(&self, db: &SqlitePool) -> Result<(), sqlx::Error> {
sqlx::query(INSERT_QUERY) sqlx::query!("insert into users (id, username, displayname, email, pwhash, invited_by) values (?, ?, ?, ?, ?, ?)",
.bind(self.id) self.id,
.bind(&self.username) self.username,
.bind(&self.displayname) self.displayname,
.bind(&self.email) self.email,
.bind(&self.pwhash) self.pwhash,
.bind(self.invited_by) self.invited_by)
.execute(db) .execute(db)
.await .await
.map(|_| ()) .map(|_| ())
} }
pub async fn update_last_seen(&self, pool: &SqlitePool) { pub async fn update_last_seen(&self, pool: &SqlitePool) {
match sqlx::query(LAST_SEEN_QUERY) match sqlx::query!(
.bind(self.id) "update users set last_seen = CURRENT_TIMESTAMP where id = ?",
.execute(pool) self.id
.await )
.execute(pool)
.await
{ {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
@ -143,15 +143,12 @@ pub async fn handle_update_last_seen(
request: Request, request: Request,
next: Next, next: Next,
) -> impl IntoResponse { ) -> impl IntoResponse {
use std::time::{SystemTime, UNIX_EPOCH};
if let Some(user) = auth.user { if let Some(user) = auth.user {
if let Some(then) = user.last_seen { if let Some(then) = &user.last_seen {
let now = SystemTime::now() let now = chrono::Utc::now();
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
// The Nyquist frequency for 1-day tracking resolution is 12 hours. // 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; user.update_last_seen(&pool).await;
} }
} else { } else {