diff --git a/migrations/2025-02-23-095541_initial/up.sql b/migrations/2025-02-23-095541_initial/up.sql index 76449a4..a9cdef5 100644 --- a/migrations/2025-02-23-095541_initial/up.sql +++ b/migrations/2025-02-23-095541_initial/up.sql @@ -12,8 +12,6 @@ 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 new file mode 100644 index 0000000..e794cf7 --- /dev/null +++ b/migrations/2025-03-15-145721_session_last_seen/down.sql @@ -0,0 +1,3 @@ +-- 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 new file mode 100644 index 0000000..776d287 --- /dev/null +++ b/migrations/2025-03-15-145721_session_last_seen/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +alter table sessions + add column last_seen bigint not null default 0; diff --git a/src/db/repository/auth.rs b/src/db/repository/auth.rs index 3aafa05..8bd7905 100644 --- a/src/db/repository/auth.rs +++ b/src/db/repository/auth.rs @@ -1,5 +1,6 @@ use chrono::DateTime; use diesel::prelude::*; +use rand::Rng; use super::SqliteRepository; use crate::{ @@ -29,6 +30,113 @@ 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/db/repository/device.rs b/src/db/repository/device.rs index 2d02c1c..18a8516 100644 --- a/src/db/repository/device.rs +++ b/src/db/repository/device.rs @@ -1,4 +1,3 @@ -use chrono::{DateTime, Utc}; use diesel::prelude::*; use super::SqliteRepository; @@ -96,35 +95,4 @@ 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 7d26b84..d8ff790 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -38,6 +38,24 @@ 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>; @@ -63,44 +81,13 @@ 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 without a sync group. + /// created. 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 { @@ -114,8 +101,7 @@ 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 and all devices in its sync group with the - /// given list + /// Replace the list of subscriptions for a device with the given list fn set_subscriptions_for_device( &self, user: &User, @@ -124,8 +110,7 @@ pub trait SubscriptionRepository { time_changed: DateTime, ) -> Result<(), AuthErr>; - /// Update the list of subscriptions for a device and all devices in its sync group by adding - /// and removing the given URLs + /// Update the list of subscriptions for a device 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 4cc17be..e55d010 100644 --- a/src/gpodder/repository.rs +++ b/src/gpodder/repository.rs @@ -94,22 +94,6 @@ 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,