diff --git a/src/cli/serve.rs b/src/cli/serve.rs index 664a1e0..a0ff0fe 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -6,10 +6,9 @@ pub fn serve(config: &crate::config::Config) -> u8 { tracing::info!("Initializing database and running migrations"); let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap(); - let repo = db::SqliteRepository::from(pool); let ctx = server::Context { - store: crate::gpodder::GpodderRepository::new(repo), + repo: db::SqliteRepository::from(pool), }; let app = server::app(ctx); diff --git a/src/db/models/session.rs b/src/db/models/session.rs index 273550a..10ee4a1 100644 --- a/src/db/models/session.rs +++ b/src/db/models/session.rs @@ -11,21 +11,16 @@ use crate::db::{schema::*, DbPool, DbResult}; pub struct Session { pub id: i64, pub user_id: i64, - pub last_seen: i64, } impl Session { - pub fn new_for_user(pool: &DbPool, user_id: i64, last_seen: i64) -> DbResult { + pub fn new_for_user(pool: &DbPool, user_id: i64) -> DbResult { let id: i64 = rand::thread_rng().gen(); - Ok(Self { - id, - user_id, - last_seen, - } - .insert_into(sessions::table) - .returning(Self::as_returning()) - .get_result(&mut pool.get()?)?) + Ok(Self { id, user_id } + .insert_into(sessions::table) + .returning(Self::as_returning()) + .get_result(&mut pool.get()?)?) } pub fn user_from_id(pool: &DbPool, id: i64) -> DbResult> { diff --git a/src/db/repository/auth.rs b/src/db/repository/auth.rs index e65f046..ff30f09 100644 --- a/src/db/repository/auth.rs +++ b/src/db/repository/auth.rs @@ -1,4 +1,3 @@ -use chrono::DateTime; use diesel::prelude::*; use rand::Rng; @@ -20,16 +19,6 @@ impl From for gpodder::AuthErr { } } -impl From for gpodder::User { - fn from(value: db::User) -> Self { - Self { - id: value.id, - username: value.username, - password_hash: value.password_hash, - } - } -} - impl gpodder::AuthRepository for SqliteRepository { fn validate_credentials( &self, @@ -46,7 +35,6 @@ impl gpodder::AuthRepository for SqliteRepository { Ok(gpodder::User { id: user.id, username: user.username, - password_hash: user.password_hash, }) } else { Err(gpodder::AuthErr::InvalidPassword) @@ -66,7 +54,6 @@ impl gpodder::AuthRepository for SqliteRepository { Ok(user) => Ok(gpodder::User { id: user.id, username: user.username, - password_hash: user.password_hash, }), Err(diesel::result::Error::NotFound) => Err(gpodder::AuthErr::UnknownSession), Err(err) => Err(gpodder::AuthErr::Other(Box::new(err))), @@ -90,7 +77,6 @@ impl gpodder::AuthRepository for SqliteRepository { let session_id = db::Session { id, user_id: user.id, - last_seen: chrono::Utc::now().timestamp(), } .insert_into(sessions::table) .returning(sessions::id) @@ -101,7 +87,6 @@ impl gpodder::AuthRepository for SqliteRepository { gpodder::User { id: user.id, username: user.username, - password_hash: user.password_hash, }, )) } else { @@ -136,49 +121,3 @@ impl gpodder::AuthRepository for SqliteRepository { } } } - -impl gpodder::AuthStore for SqliteRepository { - fn get_user(&self, username: &str) -> Result, AuthErr> { - Ok(users::table - .select(db::User::as_select()) - .filter(users::username.eq(username)) - .first(&mut self.pool.get()?) - .optional()? - .map(gpodder::User::from)) - } - - fn get_session(&self, session_id: i64) -> Result, AuthErr> { - match sessions::table - .inner_join(users::table) - .filter(sessions::id.eq(session_id)) - .select((db::Session::as_select(), db::User::as_select())) - .get_result(&mut self.pool.get()?) - { - Ok((session, user)) => Ok(Some(gpodder::Session { - id: session.id, - last_seen: DateTime::from_timestamp(session.last_seen, 0).unwrap(), - user: user.into(), - })), - Err(err) => Err(AuthErr::from(err)), - } - } - - fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> { - Ok( - diesel::delete(sessions::table.filter(sessions::id.eq(session_id))) - .execute(&mut self.pool.get()?) - .map(|_| ())?, - ) - } - - fn insert_session(&self, session: &gpodder::Session) -> Result<(), AuthErr> { - Ok(db::Session { - id: session.id, - user_id: session.user.id, - last_seen: session.last_seen.timestamp(), - } - .insert_into(sessions::table) - .execute(&mut self.pool.get()?) - .map(|_| ())?) - } -} diff --git a/src/db/repository/episode_action.rs b/src/db/repository/episode_action.rs index 5a45cd2..3f66bc9 100644 --- a/src/db/repository/episode_action.rs +++ b/src/db/repository/episode_action.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Utc}; +use chrono::DateTime; use diesel::prelude::*; use super::SqliteRepository; @@ -26,7 +26,7 @@ impl From for db::NewEpisodeAction { podcast_url: value.podcast, episode_url: value.episode, time_changed: 0, - timestamp: value.timestamp.map(|t| t.timestamp()), + timestamp: value.timestamp.map(|t| t.and_utc().timestamp()), action, started, position, @@ -58,8 +58,7 @@ impl From<(Option, db::EpisodeAction)> for gpodder::EpisodeAction { // SAFETY the input to the from_timestamp function is always the result of a // previous timestamp() function call, which is guaranteed to be each other's // reverse - .map(|ts| DateTime::from_timestamp(ts, 0).unwrap()), - time_changed: DateTime::from_timestamp(db_action.time_changed, 0).unwrap(), + .map(|ts| DateTime::from_timestamp(ts, 0).unwrap().naive_utc()), device: device_id, action, } @@ -71,9 +70,8 @@ impl gpodder::EpisodeActionRepository for SqliteRepository { &self, user: &gpodder::User, actions: Vec, - time_changed: DateTime, - ) -> Result<(), gpodder::AuthErr> { - let time_changed = time_changed.timestamp(); + ) -> Result { + let time_changed = chrono::Utc::now().timestamp(); // TODO optimize this query // 1. The lookup for a device could be replaced with a subquery, although Diesel seems to @@ -101,18 +99,17 @@ impl gpodder::EpisodeActionRepository for SqliteRepository { Ok::<_, diesel::result::Error>(()) })?; - Ok(()) + Ok(time_changed + 1) } fn episode_actions_for_user( &self, user: &gpodder::User, - since: Option>, + since: Option, podcast: Option, device: Option, aggregated: bool, - ) -> Result, gpodder::AuthErr> { - let since = since.map(|ts| ts.timestamp()).unwrap_or(0); + ) -> Result<(i64, Vec), gpodder::AuthErr> { let conn = &mut self.pool.get()?; let mut query = episode_actions::table @@ -120,7 +117,7 @@ impl gpodder::EpisodeActionRepository for SqliteRepository { .filter( episode_actions::user_id .eq(user.id) - .and(episode_actions::time_changed.ge(since)), + .and(episode_actions::time_changed.ge(since.unwrap_or(0))), ) .select(( devices::device_id.nullable(), @@ -160,11 +157,16 @@ impl gpodder::EpisodeActionRepository for SqliteRepository { query.get_results(conn)? }; + let max_timestamp = db_actions + .iter() + .map(|(_, a)| a.time_changed) + .max() + .unwrap_or(0); let actions = db_actions .into_iter() .map(gpodder::EpisodeAction::from) .collect(); - Ok(actions) + Ok((max_timestamp + 1, actions)) } } diff --git a/src/db/repository/subscription.rs b/src/db/repository/subscription.rs index 5448380..febe9dc 100644 --- a/src/db/repository/subscription.rs +++ b/src/db/repository/subscription.rs @@ -1,6 +1,5 @@ use std::collections::HashSet; -use chrono::DateTime; use diesel::prelude::*; use super::SqliteRepository; @@ -9,39 +8,24 @@ use crate::{ gpodder, }; -impl From<(String, i64)> for gpodder::Subscription { - fn from((url, ts): (String, i64)) -> Self { - Self { - url, - time_changed: DateTime::from_timestamp(ts, 0).unwrap(), - } - } -} - impl gpodder::SubscriptionRepository for SqliteRepository { fn subscriptions_for_user( &self, user: &gpodder::User, - ) -> Result, gpodder::AuthErr> { + ) -> Result, gpodder::AuthErr> { Ok(device_subscriptions::table .inner_join(devices::table) .filter(devices::user_id.eq(user.id)) - .select(( - device_subscriptions::podcast_url, - device_subscriptions::time_changed, - )) + .select(device_subscriptions::podcast_url) .distinct() - .get_results::<(String, i64)>(&mut self.pool.get()?)? - .into_iter() - .map(Into::into) - .collect()) + .get_results(&mut self.pool.get()?)?) } fn subscriptions_for_device( &self, user: &gpodder::User, device_id: &str, - ) -> Result, gpodder::AuthErr> { + ) -> Result, gpodder::AuthErr> { Ok(device_subscriptions::table .inner_join(devices::table) .filter( @@ -49,14 +33,8 @@ impl gpodder::SubscriptionRepository for SqliteRepository { .eq(user.id) .and(devices::device_id.eq(device_id)), ) - .select(( - device_subscriptions::podcast_url, - device_subscriptions::time_changed, - )) - .get_results::<(String, i64)>(&mut self.pool.get()?)? - .into_iter() - .map(Into::into) - .collect()) + .select(device_subscriptions::podcast_url) + .get_results(&mut self.pool.get()?)?) } fn set_subscriptions_for_device( @@ -64,10 +42,9 @@ impl gpodder::SubscriptionRepository for SqliteRepository { user: &gpodder::User, device_id: &str, urls: Vec, - time_changed: chrono::DateTime, - ) -> Result<(), gpodder::AuthErr> { + ) -> Result { // TODO use a better timestamp - let timestamp = time_changed.timestamp(); + let timestamp = chrono::Utc::now().timestamp(); self.pool.get()?.transaction(|conn| { let device = devices::table @@ -149,7 +126,7 @@ impl gpodder::SubscriptionRepository for SqliteRepository { Ok::<_, diesel::result::Error>(()) })?; - Ok(()) + Ok(timestamp + 1) } fn update_subscriptions_for_device( @@ -158,10 +135,9 @@ impl gpodder::SubscriptionRepository for SqliteRepository { device_id: &str, add: Vec, remove: Vec, - time_changed: chrono::DateTime, - ) -> Result<(), gpodder::AuthErr> { + ) -> Result { // TODO use a better timestamp - let timestamp = time_changed.timestamp(); + let timestamp = chrono::Utc::now().timestamp_millis(); // TODO URLs that are in both the added and removed lists will currently get "re-added", // meaning their change timestamp will be updated even though they haven't really changed. @@ -244,18 +220,16 @@ impl gpodder::SubscriptionRepository for SqliteRepository { Ok::<_, diesel::result::Error>(()) })?; - Ok(()) + Ok(timestamp + 1) } fn subscription_updates_for_device( &self, user: &gpodder::User, device_id: &str, - since: chrono::DateTime, - ) -> Result<(Vec, Vec), gpodder::AuthErr> { - let since = since.timestamp(); - - let (mut added, mut removed) = (Vec::new(), Vec::new()); + since: i64, + ) -> Result<(i64, Vec, Vec), gpodder::AuthErr> { + let (mut timestamp, mut added, mut removed) = (0, Vec::new(), Vec::new()); let query = device_subscriptions::table .inner_join(devices::table) @@ -271,18 +245,14 @@ impl gpodder::SubscriptionRepository for SqliteRepository { let sub = sub?; if sub.deleted { - removed.push(gpodder::Subscription { - url: sub.podcast_url, - time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(), - }); + removed.push(sub.podcast_url); } else { - added.push(gpodder::Subscription { - url: sub.podcast_url, - time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(), - }); + added.push(sub.podcast_url); } + + timestamp = timestamp.max(sub.time_changed); } - Ok((added, removed)) + Ok((timestamp + 1, added, removed)) } } diff --git a/src/db/schema.rs b/src/db/schema.rs index 2f597f1..fe21dfe 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -41,7 +41,6 @@ diesel::table! { sessions (id) { id -> BigInt, user_id -> BigInt, - last_seen -> BigInt, } } diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index 8336de1..4ae79d3 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -1,9 +1,6 @@ pub mod models; -mod repository; -use chrono::{DateTime, Utc}; pub use models::*; -pub use repository::GpodderRepository; pub enum AuthErr { UnknownSession, @@ -12,16 +9,6 @@ pub enum AuthErr { Other(Box), } -pub trait Store: - AuthStore + DeviceRepository + SubscriptionRepository + EpisodeActionRepository -{ -} - -impl Store for T where - T: AuthStore + DeviceRepository + SubscriptionRepository + EpisodeActionRepository -{ -} - pub trait AuthRepository { /// Validate the given session ID and return its user. fn validate_session(&self, session_id: i64) -> Result; @@ -40,20 +27,6 @@ pub trait AuthRepository { fn remove_session(&self, username: &str, session_id: i64) -> Result<(), AuthErr>; } -pub trait AuthStore { - /// Retrieve the session with the given session ID - fn get_session(&self, session_id: i64) -> Result, AuthErr>; - - /// Retrieve the user with the given username - fn get_user(&self, username: &str) -> Result, AuthErr>; - - /// Create a new session for a user with the given session ID - fn insert_session(&self, session: &Session) -> Result<(), AuthErr>; - - /// Remove the session with the given session ID - fn remove_session(&self, session_id: i64) -> Result<(), AuthErr>; -} - pub trait DeviceRepository { /// Return all devices associated with the user fn devices_for_user(&self, user: &User) -> Result, AuthErr>; @@ -74,10 +47,10 @@ pub trait SubscriptionRepository { &self, user: &User, device_id: &str, - ) -> Result, AuthErr>; + ) -> Result, AuthErr>; /// Return all subscriptions for a given user - fn subscriptions_for_user(&self, user: &User) -> Result, AuthErr>; + fn subscriptions_for_user(&self, user: &User) -> Result, AuthErr>; /// Replace the list of subscriptions for a device with the given list fn set_subscriptions_for_device( @@ -85,8 +58,7 @@ pub trait SubscriptionRepository { user: &User, device_id: &str, urls: Vec, - time_changed: DateTime, - ) -> Result<(), AuthErr>; + ) -> Result; /// Update the list of subscriptions for a device by adding and removing the given URLs fn update_subscriptions_for_device( @@ -95,34 +67,29 @@ pub trait SubscriptionRepository { device_id: &str, add: Vec, remove: Vec, - time_changed: DateTime, - ) -> Result<(), AuthErr>; + ) -> Result; /// Returns the changes in subscriptions since the given timestamp. fn subscription_updates_for_device( &self, user: &User, device_id: &str, - since: DateTime, - ) -> Result<(Vec, Vec), AuthErr>; + since: i64, + ) -> Result<(i64, Vec, Vec), AuthErr>; } pub trait EpisodeActionRepository { /// Insert the given episode actions into the datastore. - fn add_episode_actions( - &self, - user: &User, - actions: Vec, - time_changed: DateTime, - ) -> Result<(), AuthErr>; + fn add_episode_actions(&self, user: &User, actions: Vec) + -> Result; /// Retrieve the list of episode actions for the given user. fn episode_actions_for_user( &self, user: &User, - since: Option>, + since: Option, podcast: Option, device: Option, aggregated: bool, - ) -> Result, AuthErr>; + ) -> Result<(i64, Vec), AuthErr>; } diff --git a/src/gpodder/models.rs b/src/gpodder/models.rs index b68e73b..39a615c 100644 --- a/src/gpodder/models.rs +++ b/src/gpodder/models.rs @@ -1,12 +1,14 @@ -use chrono::{DateTime, Utc}; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; #[derive(Clone)] pub struct User { pub id: i64, pub username: String, - pub password_hash: String, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum DeviceType { Desktop, Laptop, @@ -15,6 +17,7 @@ pub enum DeviceType { Other, } +#[derive(Serialize)] pub struct Device { pub id: String, pub caption: String, @@ -22,38 +25,35 @@ pub struct Device { pub subscriptions: i64, } +#[derive(Deserialize)] pub struct DevicePatch { pub caption: Option, pub r#type: Option, } +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +#[serde(tag = "action")] pub enum EpisodeActionType { Download, Play { + #[serde(default)] started: Option, position: i32, + #[serde(default)] total: Option, }, Delete, New, } +#[derive(Serialize, Deserialize, Debug)] pub struct EpisodeAction { pub podcast: String, pub episode: String, - pub timestamp: Option>, - pub time_changed: DateTime, + pub timestamp: Option, + #[serde(default)] pub device: Option, + #[serde(flatten)] pub action: EpisodeActionType, } - -pub struct Session { - pub id: i64, - pub last_seen: DateTime, - pub user: User, -} - -pub struct Subscription { - pub url: String, - pub time_changed: DateTime, -} diff --git a/src/gpodder/repository.rs b/src/gpodder/repository.rs deleted file mode 100644 index 411f9cf..0000000 --- a/src/gpodder/repository.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::sync::Arc; - -use argon2::{Argon2, PasswordHash, PasswordVerifier}; -use chrono::{DateTime, TimeDelta, Utc}; -use rand::Rng; - -use super::{models, AuthErr, Store}; - -const MAX_SESSION_AGE: i64 = 60 * 60 * 24 * 7; - -#[derive(Clone)] -pub struct GpodderRepository { - store: Arc, -} - -impl GpodderRepository { - pub fn new(store: impl Store + Send + Sync + 'static) -> Self { - Self { - store: Arc::new(store), - } - } - - pub fn validate_session(&self, session_id: i64) -> Result { - let session = self - .store - .get_session(session_id)? - .ok_or(AuthErr::UnknownSession)?; - - // Expired sessions still in the database are considered removed - if Utc::now() - session.last_seen > TimeDelta::new(MAX_SESSION_AGE, 0).unwrap() { - Err(AuthErr::UnknownSession) - } else { - Ok(session) - } - } - - pub fn validate_credentials( - &self, - username: &str, - password: &str, - ) -> Result { - let user = self.store.get_user(username)?.ok_or(AuthErr::UnknownUser)?; - - let password_hash = PasswordHash::new(&user.password_hash).unwrap(); - - if Argon2::default() - .verify_password(password.as_bytes(), &password_hash) - .is_ok() - { - Ok(user) - } else { - Err(AuthErr::InvalidPassword) - } - } - - pub fn create_session(&self, user: &models::User) -> Result { - let session = models::Session { - id: rand::thread_rng().gen(), - last_seen: Utc::now(), - user: user.clone(), - }; - - self.store.insert_session(&session)?; - - Ok(session) - } - - pub fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> { - self.store.remove_session(session_id) - } - - pub fn devices_for_user(&self, user: &models::User) -> Result, AuthErr> { - self.store.devices_for_user(user) - } - - pub fn update_device_info( - &self, - user: &models::User, - device_id: &str, - patch: models::DevicePatch, - ) -> Result<(), AuthErr> { - self.store.update_device_info(user, device_id, patch) - } - - pub fn subscriptions_for_device( - &self, - user: &models::User, - device_id: &str, - ) -> Result, AuthErr> { - self.store.subscriptions_for_device(user, device_id) - } - - pub fn subscriptions_for_user( - &self, - user: &models::User, - ) -> Result, AuthErr> { - self.store.subscriptions_for_user(user) - } - - pub fn set_subscriptions_for_device( - &self, - user: &models::User, - device_id: &str, - urls: Vec, - ) -> Result, AuthErr> { - let time_changed = Utc::now(); - - self.store - .set_subscriptions_for_device(user, device_id, urls, time_changed)?; - - Ok(time_changed + TimeDelta::seconds(1)) - } - - pub fn update_subscriptions_for_device( - &self, - user: &models::User, - device_id: &str, - add: Vec, - remove: Vec, - ) -> Result, AuthErr> { - let time_changed = Utc::now(); - - self.store - .update_subscriptions_for_device(user, device_id, add, remove, time_changed)?; - - Ok(time_changed + TimeDelta::seconds(1)) - } - - pub fn subscription_updates_for_device( - &self, - user: &models::User, - device_id: &str, - since: DateTime, - ) -> Result< - ( - DateTime, - Vec, - Vec, - ), - AuthErr, - > { - let now = chrono::Utc::now(); - - let (added, removed) = self - .store - .subscription_updates_for_device(user, device_id, since)?; - - let max_time_changed = added - .iter() - .chain(removed.iter()) - .map(|s| s.time_changed) - .max() - .unwrap_or(now); - Ok((max_time_changed + TimeDelta::seconds(1), added, removed)) - } - - pub fn add_episode_actions( - &self, - user: &models::User, - actions: Vec, - ) -> Result, AuthErr> { - let time_changed = Utc::now(); - - self.store - .add_episode_actions(user, actions, time_changed)?; - - Ok(time_changed + TimeDelta::seconds(1)) - } - - pub fn episode_actions_for_user( - &self, - user: &models::User, - since: Option>, - podcast: Option, - device: Option, - aggregated: bool, - ) -> Result<(DateTime, Vec), AuthErr> { - let now = chrono::Utc::now(); - let actions = self - .store - .episode_actions_for_user(user, since, podcast, device, aggregated)?; - let max_time_changed = actions.iter().map(|a| a.time_changed).max().unwrap_or(now); - - Ok((max_time_changed + TimeDelta::seconds(1), actions)) - } -} diff --git a/src/server/gpodder/advanced/auth.rs b/src/server/gpodder/advanced/auth.rs index ac5cbe6..abdb0d6 100644 --- a/src/server/gpodder/advanced/auth.rs +++ b/src/server/gpodder/advanced/auth.rs @@ -12,10 +12,13 @@ use axum_extra::{ TypedHeader, }; -use crate::server::{ - error::{AppError, AppResult}, - gpodder::SESSION_ID_COOKIE, - Context, +use crate::{ + gpodder::AuthRepository, + server::{ + error::{AppError, AppResult}, + gpodder::SESSION_ID_COOKIE, + Context, + }, }; pub fn router() -> Router { @@ -35,17 +38,14 @@ async fn post_login( return Err(AppError::BadRequest); } - let session = tokio::task::spawn_blocking(move || { - let user = ctx - .store - .validate_credentials(auth.username(), auth.password())?; - ctx.store.create_session(&user) + let (session_id, _) = tokio::task::spawn_blocking(move || { + ctx.repo.create_session(auth.username(), auth.password()) }) .await .unwrap()?; Ok(jar.add( - Cookie::build((SESSION_ID_COOKIE, session.id.to_string())).expires(Expiration::Session), + Cookie::build((SESSION_ID_COOKIE, session_id.to_string())).expires(Expiration::Session), )) } @@ -60,8 +60,7 @@ async fn post_logout( .parse() .map_err(|_| AppError::BadRequest)?; - // TODO reintroduce username check - tokio::task::spawn_blocking(move || ctx.store.remove_session(session_id)) + tokio::task::spawn_blocking(move || ctx.repo.remove_session(&username, session_id)) .await .unwrap()?; diff --git a/src/server/gpodder/advanced/devices.rs b/src/server/gpodder/advanced/devices.rs index 859e1c1..bb90dc2 100644 --- a/src/server/gpodder/advanced/devices.rs +++ b/src/server/gpodder/advanced/devices.rs @@ -6,13 +6,12 @@ use axum::{ }; use crate::{ - gpodder, + gpodder::{self, DeviceRepository}, server::{ error::{AppError, AppResult}, gpodder::{ auth_middleware, format::{Format, StringWithFormat}, - models, }, Context, }, @@ -29,7 +28,7 @@ async fn get_devices( State(ctx): State, Path(username): Path, Extension(user): Extension, -) -> AppResult>> { +) -> AppResult>> { if username.format != Format::Json { return Err(AppError::NotFound); } @@ -39,10 +38,10 @@ async fn get_devices( } Ok( - tokio::task::spawn_blocking(move || ctx.store.devices_for_user(&user)) + tokio::task::spawn_blocking(move || ctx.repo.devices_for_user(&user)) .await .unwrap() - .map(|devices| Json(devices.into_iter().map(models::Device::from).collect()))?, + .map(Json)?, ) } @@ -50,13 +49,13 @@ async fn post_device( State(ctx): State, Path((_username, id)): Path<(String, StringWithFormat)>, Extension(user): Extension, - Json(patch): Json, + Json(patch): Json, ) -> AppResult<()> { if id.format != Format::Json { return Err(AppError::NotFound); } - tokio::task::spawn_blocking(move || ctx.store.update_device_info(&user, &id, patch.into())) + tokio::task::spawn_blocking(move || ctx.repo.update_device_info(&user, &id, patch)) .await .unwrap()?; diff --git a/src/server/gpodder/advanced/episodes.rs b/src/server/gpodder/advanced/episodes.rs index 6c7ff32..b862b9c 100644 --- a/src/server/gpodder/advanced/episodes.rs +++ b/src/server/gpodder/advanced/episodes.rs @@ -4,17 +4,15 @@ use axum::{ routing::post, Extension, Json, Router, }; -use chrono::DateTime; use serde::{Deserialize, Serialize}; use crate::{ - gpodder, + gpodder::{self, EpisodeActionRepository}, server::{ error::{AppError, AppResult}, gpodder::{ auth_middleware, format::{Format, StringWithFormat}, - models, models::UpdatedUrlsResponse, }, Context, @@ -34,7 +32,7 @@ async fn post_episode_actions( State(ctx): State, Path(username): Path, Extension(user): Extension, - Json(actions): Json>, + Json(actions): Json>, ) -> AppResult> { if username.format != Format::Json { return Err(AppError::NotFound); @@ -44,18 +42,17 @@ async fn post_episode_actions( return Err(AppError::BadRequest); } - Ok(tokio::task::spawn_blocking(move || { - ctx.store - .add_episode_actions(&user, actions.into_iter().map(Into::into).collect()) - }) - .await - .unwrap() - .map(|time_changed| { - Json(UpdatedUrlsResponse { - timestamp: time_changed.timestamp(), - update_urls: Vec::new(), - }) - })?) + Ok( + tokio::task::spawn_blocking(move || ctx.repo.add_episode_actions(&user, actions)) + .await + .unwrap() + .map(|timestamp| { + Json(UpdatedUrlsResponse { + timestamp, + update_urls: Vec::new(), + }) + })?, + ) } #[derive(Deserialize, Default)] @@ -70,7 +67,7 @@ struct FilterQuery { #[derive(Serialize)] struct EpisodeActionsResponse { timestamp: i64, - actions: Vec, + actions: Vec, } async fn get_episode_actions( @@ -87,15 +84,10 @@ async fn get_episode_actions( return Err(AppError::BadRequest); } - let since = filter - .since - .map(|ts| DateTime::from_timestamp(ts, 0)) - .flatten(); - Ok(tokio::task::spawn_blocking(move || { - ctx.store.episode_actions_for_user( + ctx.repo.episode_actions_for_user( &user, - since, + filter.since, filter.podcast, filter.device, filter.aggregated, @@ -103,10 +95,5 @@ async fn get_episode_actions( }) .await .unwrap() - .map(|(ts, actions)| { - Json(EpisodeActionsResponse { - timestamp: ts.timestamp(), - actions: actions.into_iter().map(Into::into).collect(), - }) - })?) + .map(|(timestamp, actions)| Json(EpisodeActionsResponse { timestamp, actions }))?) } diff --git a/src/server/gpodder/advanced/subscriptions.rs b/src/server/gpodder/advanced/subscriptions.rs index e5691e1..98d464a 100644 --- a/src/server/gpodder/advanced/subscriptions.rs +++ b/src/server/gpodder/advanced/subscriptions.rs @@ -7,7 +7,7 @@ use axum::{ use serde::Deserialize; use crate::{ - gpodder, + gpodder::{self, SubscriptionRepository}, server::{ error::{AppError, AppResult}, gpodder::{ @@ -43,14 +43,14 @@ pub async fn post_subscription_changes( } Ok(tokio::task::spawn_blocking(move || { - ctx.store + ctx.repo .update_subscriptions_for_device(&user, &id, delta.add, delta.remove) }) .await .unwrap() - .map(|time_changed| { + .map(|timestamp| { Json(UpdatedUrlsResponse { - timestamp: time_changed.timestamp(), + timestamp, update_urls: Vec::new(), }) })?) @@ -76,18 +76,17 @@ pub async fn get_subscription_changes( return Err(AppError::BadRequest); } - let since = chrono::DateTime::from_timestamp(query.since, 0).ok_or(AppError::BadRequest)?; - Ok(tokio::task::spawn_blocking(move || { - ctx.store.subscription_updates_for_device(&user, &id, since) + ctx.repo + .subscription_updates_for_device(&user, &id, query.since) }) .await .unwrap() - .map(|(next_time_changed, add, remove)| { + .map(|(timestamp, add, remove)| { Json(SubscriptionDeltaResponse { - add: add.into_iter().map(|s| s.url).collect(), - remove: remove.into_iter().map(|s| s.url).collect(), - timestamp: next_time_changed.timestamp(), + add, + remove, + timestamp, }) })?) } diff --git a/src/server/gpodder/mod.rs b/src/server/gpodder/mod.rs index 776100e..e98b82b 100644 --- a/src/server/gpodder/mod.rs +++ b/src/server/gpodder/mod.rs @@ -17,7 +17,10 @@ use axum_extra::{ }; use tower_http::set_header::SetResponseHeaderLayer; -use crate::{gpodder, server::error::AppError}; +use crate::{ + gpodder::{self, AuthRepository}, + server::error::AppError, +}; use super::Context; @@ -38,6 +41,8 @@ pub fn router(ctx: Context) -> Router { /// This middleware accepts pub async fn auth_middleware(State(ctx): State, mut req: Request, next: Next) -> Response { + tracing::debug!("{:?}", req.headers()); + // SAFETY: this extractor's error type is Infallible let mut jar: CookieJar = req.extract_parts().await.unwrap(); let mut auth_user = None; @@ -48,12 +53,12 @@ pub async fn auth_middleware(State(ctx): State, mut req: Request, next: .and_then(|c| c.value().parse::().ok()) { let ctx_clone = ctx.clone(); - match tokio::task::spawn_blocking(move || ctx_clone.store.validate_session(session_id)) + match tokio::task::spawn_blocking(move || ctx_clone.repo.validate_session(session_id)) .await .unwrap() { - Ok(session) => { - auth_user = Some(session.user); + Ok(user) => { + auth_user = Some(user); } Err(gpodder::AuthErr::UnknownSession) => { jar = jar.add( @@ -74,7 +79,7 @@ pub async fn auth_middleware(State(ctx): State, mut req: Request, next: .await { match tokio::task::spawn_blocking(move || { - ctx.store + ctx.repo .validate_credentials(auth.username(), auth.password()) }) .await diff --git a/src/server/gpodder/models.rs b/src/server/gpodder/models.rs index b2268af..2be8f87 100644 --- a/src/server/gpodder/models.rs +++ b/src/server/gpodder/models.rs @@ -1,8 +1,5 @@ -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use crate::gpodder; - #[derive(Deserialize, Debug)] pub struct SubscriptionDelta { pub add: Vec, @@ -21,164 +18,3 @@ pub struct UpdatedUrlsResponse { pub timestamp: i64, pub update_urls: Vec<(String, String)>, } - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum DeviceType { - Desktop, - Laptop, - Mobile, - Server, - Other, -} - -#[derive(Serialize)] -pub struct Device { - pub id: String, - pub caption: String, - pub r#type: DeviceType, - pub subscriptions: i64, -} - -#[derive(Deserialize)] -pub struct DevicePatch { - pub caption: Option, - pub r#type: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "lowercase")] -#[serde(tag = "action")] -pub enum EpisodeActionType { - Download, - Play { - #[serde(default)] - started: Option, - position: i32, - #[serde(default)] - total: Option, - }, - Delete, - New, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct EpisodeAction { - pub podcast: String, - pub episode: String, - pub timestamp: Option, - #[serde(default)] - pub device: Option, - #[serde(flatten)] - pub action: EpisodeActionType, -} - -impl From for DeviceType { - fn from(value: gpodder::DeviceType) -> Self { - match value { - gpodder::DeviceType::Other => Self::Other, - gpodder::DeviceType::Laptop => Self::Laptop, - gpodder::DeviceType::Mobile => Self::Mobile, - gpodder::DeviceType::Server => Self::Server, - gpodder::DeviceType::Desktop => Self::Desktop, - } - } -} - -impl From for gpodder::DeviceType { - fn from(value: DeviceType) -> Self { - match value { - DeviceType::Other => gpodder::DeviceType::Other, - DeviceType::Laptop => gpodder::DeviceType::Laptop, - DeviceType::Mobile => gpodder::DeviceType::Mobile, - DeviceType::Server => gpodder::DeviceType::Server, - DeviceType::Desktop => gpodder::DeviceType::Desktop, - } - } -} - -impl From for Device { - fn from(value: gpodder::Device) -> Self { - Self { - id: value.id, - caption: value.caption, - r#type: value.r#type.into(), - subscriptions: value.subscriptions, - } - } -} - -impl From for gpodder::DevicePatch { - fn from(value: DevicePatch) -> Self { - Self { - caption: value.caption, - r#type: value.r#type.map(Into::into), - } - } -} - -impl From for EpisodeActionType { - fn from(value: gpodder::EpisodeActionType) -> Self { - match value { - gpodder::EpisodeActionType::New => Self::New, - gpodder::EpisodeActionType::Delete => Self::Delete, - gpodder::EpisodeActionType::Download => Self::Download, - gpodder::EpisodeActionType::Play { - started, - position, - total, - } => Self::Play { - started, - position, - total, - }, - } - } -} - -impl From for gpodder::EpisodeActionType { - fn from(value: EpisodeActionType) -> Self { - match value { - EpisodeActionType::New => gpodder::EpisodeActionType::New, - EpisodeActionType::Delete => gpodder::EpisodeActionType::Delete, - EpisodeActionType::Download => gpodder::EpisodeActionType::Download, - EpisodeActionType::Play { - started, - position, - total, - } => gpodder::EpisodeActionType::Play { - started, - position, - total, - }, - } - } -} - -impl From for EpisodeAction { - fn from(value: gpodder::EpisodeAction) -> Self { - Self { - podcast: value.podcast, - episode: value.episode, - timestamp: value.timestamp.map(|ts| ts.timestamp()), - device: value.device, - action: value.action.into(), - } - } -} - -impl From for gpodder::EpisodeAction { - fn from(value: EpisodeAction) -> Self { - Self { - podcast: value.podcast, - episode: value.episode, - // TODO remove this unwrap - timestamp: value - .timestamp - .map(|ts| DateTime::from_timestamp(ts, 0).unwrap()), - device: value.device, - action: value.action.into(), - time_changed: DateTime::::MIN_UTC, - } - } -} diff --git a/src/server/gpodder/simple/subscriptions.rs b/src/server/gpodder/simple/subscriptions.rs index 6e9227c..4e6f7c6 100644 --- a/src/server/gpodder/simple/subscriptions.rs +++ b/src/server/gpodder/simple/subscriptions.rs @@ -6,7 +6,7 @@ use axum::{ }; use crate::{ - gpodder, + gpodder::{self, SubscriptionRepository}, server::{ error::{AppError, AppResult}, gpodder::{auth_middleware, format::StringWithFormat}, @@ -34,10 +34,10 @@ pub async fn get_device_subscriptions( } Ok( - tokio::task::spawn_blocking(move || ctx.store.subscriptions_for_device(&user, &id)) + tokio::task::spawn_blocking(move || ctx.repo.subscriptions_for_device(&user, &id)) .await .unwrap() - .map(|subs| Json(subs.into_iter().map(|s| s.url).collect()))?, + .map(Json)?, ) } @@ -51,10 +51,10 @@ pub async fn get_user_subscriptions( } Ok( - tokio::task::spawn_blocking(move || ctx.store.subscriptions_for_user(&user)) + tokio::task::spawn_blocking(move || ctx.repo.subscriptions_for_user(&user)) .await .unwrap() - .map(|subs| Json(subs.into_iter().map(|s| s.url).collect()))?, + .map(Json)?, ) } @@ -68,10 +68,12 @@ pub async fn put_device_subscriptions( return Err(AppError::BadRequest); } - Ok(tokio::task::spawn_blocking(move || { - ctx.store.set_subscriptions_for_device(&user, &id, urls) - }) - .await - .unwrap() - .map(|_| ())?) + Ok( + tokio::task::spawn_blocking(move || { + ctx.repo.set_subscriptions_for_device(&user, &id, urls) + }) + .await + .unwrap() + .map(|_| ())?, + ) } diff --git a/src/server/mod.rs b/src/server/mod.rs index 93ac8d3..9a8c62e 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,28 +1,17 @@ mod error; mod gpodder; -use axum::{extract::Request, middleware::Next, response::Response, Router}; +use axum::Router; use tower_http::trace::TraceLayer; #[derive(Clone)] pub struct Context { - pub store: crate::gpodder::GpodderRepository, + pub repo: crate::db::SqliteRepository, } pub fn app(ctx: Context) -> Router { Router::new() .merge(gpodder::router(ctx.clone())) - .layer(axum::middleware::from_fn(header_logger)) .layer(TraceLayer::new_for_http()) .with_state(ctx) } - -async fn header_logger(request: Request, next: Next) -> Response { - tracing::debug!("request headers = {:?}", request.headers()); - - let res = next.run(request).await; - - tracing::debug!("response headers = {:?}", res.headers()); - - res -}