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",
"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",

View file

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

View file

@ -1,3 +1,4 @@
drop view if exists q;
drop view if exists i;
drop view if exists u;
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 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;

View file

@ -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()))
}

View file

@ -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,
}
}
}

View file

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

View file

@ -22,6 +22,7 @@ pub use users::User;
pub use watches::{ShowKind, Watch, WatchQuest};
pub type WWRouter = axum::Router<SqlitePool>;
pub type WatchDate = chrono::DateTime<chrono::Utc>;
// everything else is private to the crate
mod auth;
@ -55,6 +56,9 @@ pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService<axum::Router> {
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());

View file

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

View file

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

View file

@ -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<i64>,
expires_at: Option<WatchDate>,
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
}
}

View file

@ -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<String>,
pub email: Option<String>,
pub last_seen: Option<i64>,
pub last_seen: Option<WatchDate>,
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<Option<Self>, 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 {