From 12c1c72d4d3276812e63228f05803e7c938a31f4 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 16 Mar 2025 14:05:29 +0100 Subject: [PATCH 1/3] chore: merge session last seen migration into initial --- migrations/2025-02-23-095541_initial/up.sql | 2 ++ migrations/2025-03-15-145721_session_last_seen/down.sql | 3 --- migrations/2025-03-15-145721_session_last_seen/up.sql | 3 --- 3 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 migrations/2025-03-15-145721_session_last_seen/down.sql delete mode 100644 migrations/2025-03-15-145721_session_last_seen/up.sql diff --git a/migrations/2025-02-23-095541_initial/up.sql b/migrations/2025-02-23-095541_initial/up.sql index a9cdef5..76449a4 100644 --- a/migrations/2025-02-23-095541_initial/up.sql +++ b/migrations/2025-02-23-095541_initial/up.sql @@ -12,6 +12,8 @@ create table sessions ( references users (id) on delete cascade, + last_seen bigint not null, + unique (id, user_id) ); diff --git a/migrations/2025-03-15-145721_session_last_seen/down.sql b/migrations/2025-03-15-145721_session_last_seen/down.sql deleted file mode 100644 index e794cf7..0000000 --- a/migrations/2025-03-15-145721_session_last_seen/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -alter table sessions - drop column last_seen; diff --git a/migrations/2025-03-15-145721_session_last_seen/up.sql b/migrations/2025-03-15-145721_session_last_seen/up.sql deleted file mode 100644 index 776d287..0000000 --- a/migrations/2025-03-15-145721_session_last_seen/up.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Your SQL goes here -alter table sessions - add column last_seen bigint not null default 0; From 0849c88796a02469c1da91c1920f0e36d0a3000d Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 16 Mar 2025 14:07:24 +0100 Subject: [PATCH 2/3] chore: remove old AuthRepository --- src/db/repository/auth.rs | 108 -------------------------------------- src/gpodder/mod.rs | 18 ------- 2 files changed, 126 deletions(-) diff --git a/src/db/repository/auth.rs b/src/db/repository/auth.rs index 8bd7905..3aafa05 100644 --- a/src/db/repository/auth.rs +++ b/src/db/repository/auth.rs @@ -1,6 +1,5 @@ use chrono::DateTime; use diesel::prelude::*; -use rand::Rng; use super::SqliteRepository; use crate::{ @@ -30,113 +29,6 @@ impl From for gpodder::User { } } -impl gpodder::AuthRepository for SqliteRepository { - fn validate_credentials( - &self, - username: &str, - password: &str, - ) -> Result { - if let Some(user) = users::table - .select(db::User::as_select()) - .filter(users::username.eq(username)) - .first(&mut self.pool.get()?) - .optional()? - { - if user.verify_password(password) { - Ok(gpodder::User { - id: user.id, - username: user.username, - password_hash: user.password_hash, - }) - } else { - Err(gpodder::AuthErr::InvalidPassword) - } - } else { - Err(gpodder::AuthErr::UnknownUser) - } - } - - fn validate_session(&self, session_id: i64) -> Result { - match sessions::dsl::sessions - .inner_join(users::table) - .filter(sessions::id.eq(session_id)) - .select(db::User::as_select()) - .get_result(&mut self.pool.get()?) - { - 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))), - } - } - - fn create_session( - &self, - username: &str, - password: &str, - ) -> Result<(i64, gpodder::models::User), gpodder::AuthErr> { - if let Some(user) = users::table - .select(db::User::as_select()) - .filter(users::username.eq(username)) - .first(&mut self.pool.get()?) - .optional()? - { - if user.verify_password(password) { - let id: i64 = rand::thread_rng().gen(); - - let session_id = db::Session { - id, - user_id: user.id, - last_seen: chrono::Utc::now().timestamp(), - } - .insert_into(sessions::table) - .returning(sessions::id) - .get_result(&mut self.pool.get()?)?; - - Ok(( - session_id, - gpodder::User { - id: user.id, - username: user.username, - password_hash: user.password_hash, - }, - )) - } else { - Err(gpodder::AuthErr::InvalidPassword) - } - } else { - Err(gpodder::AuthErr::UnknownUser) - } - } - - fn remove_session(&self, username: &str, session_id: i64) -> Result<(), gpodder::AuthErr> { - let conn = &mut self.pool.get()?; - - if let Some(user) = sessions::table - .inner_join(users::table) - .filter(sessions::id.eq(session_id)) - .select(db::User::as_select()) - .get_result(conn) - .optional()? - { - if user.username == username { - Ok( - diesel::delete(sessions::table.filter(sessions::id.eq(session_id))) - .execute(conn) - .map(|_| ())?, - ) - } else { - Err(AuthErr::UnknownUser) - } - } else { - Ok(()) - } - } -} - impl gpodder::AuthStore for SqliteRepository { fn get_user(&self, username: &str) -> Result, AuthErr> { Ok(users::table diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index d8ff790..fe904d4 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -38,24 +38,6 @@ impl Store for T where { } -pub trait AuthRepository { - /// Validate the given session ID and return its user. - fn validate_session(&self, session_id: i64) -> Result; - - /// Validate the credentials, returning the user if the credentials are correct. - fn validate_credentials(&self, username: &str, password: &str) - -> Result; - - /// Create a new session for the given user. - fn create_session( - &self, - username: &str, - password: &str, - ) -> Result<(i64, models::User), AuthErr>; - - 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>; From 320a46c0f313e6ba6f048f94d6635ac42362bc6e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 16 Mar 2025 15:30:21 +0100 Subject: [PATCH 3/3] feat: define device sync abstraction API --- src/db/repository/device.rs | 32 ++++++++++++++++++++++++++++++ src/gpodder/mod.rs | 39 ++++++++++++++++++++++++++++++++++--- src/gpodder/repository.rs | 16 +++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/db/repository/device.rs b/src/db/repository/device.rs index 18a8516..2d02c1c 100644 --- a/src/db/repository/device.rs +++ b/src/db/repository/device.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, Utc}; use diesel::prelude::*; use super::SqliteRepository; @@ -95,4 +96,35 @@ impl gpodder::DeviceRepository for SqliteRepository { Ok(()) } + + fn merge_sync_groups( + &self, + user: &gpodder::User, + device_ids: Vec<&str>, + ) -> Result { + todo!() + } + + fn remove_from_sync_group( + &self, + user: &gpodder::User, + device_ids: Vec<&str>, + ) -> Result<(), gpodder::AuthErr> { + todo!() + } + + fn synchronize_sync_group( + &self, + group_id: i64, + time_changed: DateTime, + ) -> Result<(), gpodder::AuthErr> { + todo!() + } + + fn devices_by_sync_group( + &self, + user: &gpodder::User, + ) -> Result<(Vec, Vec>), gpodder::AuthErr> { + todo!() + } } diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index fe904d4..7d26b84 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -63,13 +63,44 @@ pub trait DeviceRepository { fn devices_for_user(&self, user: &User) -> Result, AuthErr>; /// Update the information for the given device. If the device doesn't exist yet, it should be - /// created. + /// created without a sync group. fn update_device_info( &self, user: &User, device_id: &str, patch: DevicePatch, ) -> Result<(), AuthErr>; + + /// Add the devices to the same sync group by: + /// + /// * Merging the sync groups of all devices already in a sync group + /// * Adding all devices not yet in a sync group to the newly merged sync group + /// + /// # Returns + /// + /// ID of the final sync group + fn merge_sync_groups(&self, user: &User, device_ids: Vec<&str>) -> Result; + + /// Synchronize the sync group by adding or removing subscriptions to each device so that each + /// device's subscription state is the same + fn synchronize_sync_group( + &self, + group_id: i64, + time_changed: DateTime, + ) -> Result<(), AuthErr>; + + /// Remove the devices from their respective sync groups + fn remove_from_sync_group(&self, user: &User, device_ids: Vec<&str>) -> Result<(), AuthErr>; + + /// Return all devices for the user, grouped per sync group + /// + /// # Returns + /// + /// A tuple (unsynced devices, sync groups) + fn devices_by_sync_group( + &self, + user: &User, + ) -> Result<(Vec, Vec>), AuthErr>; } pub trait SubscriptionRepository { @@ -83,7 +114,8 @@ pub trait SubscriptionRepository { /// Return all subscriptions for a given user fn subscriptions_for_user(&self, user: &User) -> Result, AuthErr>; - /// Replace the list of subscriptions for a device with the given list + /// Replace the list of subscriptions for a device and all devices in its sync group with the + /// given list fn set_subscriptions_for_device( &self, user: &User, @@ -92,7 +124,8 @@ pub trait SubscriptionRepository { time_changed: DateTime, ) -> Result<(), AuthErr>; - /// Update the list of subscriptions for a device by adding and removing the given URLs + /// Update the list of subscriptions for a device and all devices in its sync group by adding + /// and removing the given URLs fn update_subscriptions_for_device( &self, user: &User, diff --git a/src/gpodder/repository.rs b/src/gpodder/repository.rs index e55d010..4cc17be 100644 --- a/src/gpodder/repository.rs +++ b/src/gpodder/repository.rs @@ -94,6 +94,22 @@ impl GpodderRepository { self.store.update_device_info(user, device_id, patch) } + pub fn update_device_sync_status( + &self, + user: &models::User, + sync: Vec>, + unsync: Vec<&str>, + ) -> Result<(), AuthErr> { + todo!("perform diff devices to sync and unsync") + } + + pub fn devices_by_sync_group( + &self, + user: &models::User, + ) -> Result<(Vec, Vec>), AuthErr> { + self.store.devices_by_sync_group(user) + } + pub fn subscriptions_for_device( &self, user: &models::User,