fix date handling.
This commit is contained in:
parent
4d8706d6bf
commit
97623afd8d
12 changed files with 61 additions and 40 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
drop view if exists q;
|
||||
drop view if exists i;
|
||||
drop view if exists u;
|
||||
drop view if exists s;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
49
src/users.rs
49
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<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 {
|
||||
|
|
Loading…
Reference in a new issue