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",
|
"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",
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
45
src/users.rs
45
src/users.rs
|
@ -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,28 +84,33 @@ 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 = ?",
|
||||||
|
self.id
|
||||||
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue