diff --git a/gpodder/src/models.rs b/gpodder/src/models.rs index fb9339b..2f77ba1 100644 --- a/gpodder/src/models.rs +++ b/gpodder/src/models.rs @@ -5,7 +5,6 @@ pub struct User { pub id: i64, pub username: String, pub password_hash: String, - pub admin: bool, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/gpodder/src/repository/authenticated.rs b/gpodder/src/repository.rs similarity index 51% rename from gpodder/src/repository/authenticated.rs rename to gpodder/src/repository.rs index 9dd05ee..5c086e6 100644 --- a/gpodder/src/repository/authenticated.rs +++ b/gpodder/src/repository.rs @@ -1,47 +1,93 @@ -use std::collections::HashSet; +use std::{collections::HashSet, sync::Arc}; +use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString}; use chrono::{DateTime, TimeDelta, Utc}; -use rand::Rng; +use rand::{Rng, rngs::OsRng}; -use crate::{AuthErr, GpodderStore, models}; +use crate::{ + models, + store::{AuthErr, GpodderStore}, +}; -/// Authenticated view of the repository, providing methods that take the authenticated user -/// explicitely into account -pub struct AuthenticatedRepository<'a> { - pub(crate) store: &'a (dyn GpodderStore + Send + Sync), - pub(crate) user: &'a models::User, +const MAX_SESSION_AGE: i64 = 60 * 60 * 24 * 7; + +#[derive(Clone)] +pub struct GpodderRepository { + store: Arc, } -impl<'a> AuthenticatedRepository<'a> { - /// Retrieve the given session from the database, if it exists and is visible to the user +impl GpodderRepository { + pub fn new(store: impl GpodderStore + Send + Sync + 'static) -> Self { + Self { + store: Arc::new(store), + } + } + pub fn get_session(&self, session_id: i64) -> Result { let session = self .store .get_session(session_id)? .ok_or(AuthErr::UnknownSession)?; - // Users can't see sessions from other users, and expired sessions still in the database - // are considered removed - if session.user.id != self.user.id - || Utc::now() - session.last_seen > super::MAX_SESSION_AGE - { + // 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) } } - /// Retrieve a paginated list of the user's sessions - pub fn paginated_sessions(&self, page: models::Page) -> Result, AuthErr> { - self.store.paginated_sessions(self.user, page) + pub fn paginated_sessions( + &self, + user: &models::User, + page: models::Page, + ) -> Result, AuthErr> { + self.store.paginated_sessions(user, page) } - /// Create a new session for the authenticated user - pub fn create_session(&self, user_agent: Option) -> Result { + pub fn get_user(&self, username: &str) -> Result { + self.store.get_user(username)?.ok_or(AuthErr::UnknownUser) + } + + pub fn create_user(&self, username: &str, password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + + let password_hash = Argon2::default() + .hash_password(password.as_bytes(), &salt) + .unwrap() + .to_string(); + + self.store.insert_user(username, &password_hash) + } + + 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, + user_agent: Option, + ) -> Result { let session = models::Session { id: rand::thread_rng().r#gen(), last_seen: Utc::now(), - user: self.user.clone(), + user: user.clone(), user_agent, }; @@ -50,38 +96,38 @@ impl<'a> AuthenticatedRepository<'a> { Ok(session) } - /// Set the session's last seen value to the current time pub fn refresh_session(&self, session: &models::Session) -> Result<(), AuthErr> { let now = Utc::now(); self.store.refresh_session(session, now) } - /// Remove the given session, if it belongs to the authenticated user pub fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> { - // This fails if the session doesn't exist for the user, so it's basically a "exists" check - let session = self.get_session(session_id)?; - - self.store.remove_session(session.id) + self.store.remove_session(session_id) } - /// Return the devices for the authenticated user - pub fn devices(&self) -> Result, AuthErr> { - self.store.devices_for_user(self.user) + pub fn remove_old_sessions(&self) -> Result { + let min_last_seen = Utc::now() - TimeDelta::seconds(MAX_SESSION_AGE); + + self.store.remove_old_sessions(min_last_seen) + } + + pub fn devices_for_user(&self, user: &models::User) -> Result, AuthErr> { + self.store.devices_for_user(user) } - /// Update the metadata of a device pub fn update_device_info( &self, + user: &models::User, device_id: &str, patch: models::DevicePatch, ) -> Result<(), AuthErr> { - self.store.update_device_info(self.user, device_id, patch) + self.store.update_device_info(user, device_id, patch) } - /// Update the sync status for some of the user's devices pub fn update_device_sync_status( &self, + user: &models::User, sync: Vec>, unsync: Vec<&str>, ) -> Result<(), AuthErr> { @@ -100,72 +146,71 @@ impl<'a> AuthenticatedRepository<'a> { unsync.remove(device_id); } - let group_id = self.store.merge_sync_groups(self.user, remaining)?; + let group_id = self.store.merge_sync_groups(user, remaining)?; self.store.synchronize_sync_group(group_id, now)?; } // Finally we unsync the remaining devices self.store - .remove_from_sync_group(self.user, unsync.into_iter().collect())?; + .remove_from_sync_group(user, unsync.into_iter().collect())?; Ok(()) } - /// Return the user's devices, grouped per sync group - pub fn devices_by_sync_group(&self) -> Result<(Vec, Vec>), AuthErr> { - self.store.devices_by_sync_group(self.user) + pub fn devices_by_sync_group( + &self, + user: &models::User, + ) -> Result<(Vec, Vec>), AuthErr> { + self.store.devices_by_sync_group(user) } - /// Retrieve the user's subscriptions for a device pub fn subscriptions_for_device( &self, + user: &models::User, device_id: &str, ) -> Result, AuthErr> { - self.store.subscriptions_for_device(self.user, device_id) + self.store.subscriptions_for_device(user, device_id) } - /// Retrieve the user's subscriptions - pub fn subscriptions(&self) -> Result, AuthErr> { - self.store.subscriptions_for_user(self.user) + pub fn subscriptions_for_user( + &self, + user: &models::User, + ) -> Result, AuthErr> { + self.store.subscriptions_for_user(user) } - /// Set the subscriptions for a given device 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(self.user, device_id, urls, time_changed)?; + .set_subscriptions_for_device(user, device_id, urls, time_changed)?; Ok(time_changed + TimeDelta::seconds(1)) } - /// Add and remove subscriptions to and from a given device. 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( - self.user, - device_id, - add, - remove, - time_changed, - )?; + self.store + .update_subscriptions_for_device(user, device_id, add, remove, time_changed)?; Ok(time_changed + TimeDelta::seconds(1)) } - /// Get the changes in subscriptions for a given device after a given timestamp. pub fn subscription_updates_for_device( &self, + user: &models::User, device_id: &str, since: DateTime, ) -> Result< @@ -180,7 +225,7 @@ impl<'a> AuthenticatedRepository<'a> { let (added, removed) = self .store - .subscription_updates_for_device(self.user, device_id, since)?; + .subscription_updates_for_device(user, device_id, since)?; let max_time_changed = added .iter() @@ -191,22 +236,22 @@ impl<'a> AuthenticatedRepository<'a> { Ok((max_time_changed + TimeDelta::seconds(1), added, removed)) } - /// Add episode actions to the database pub fn add_episode_actions( &self, + user: &models::User, actions: Vec, ) -> Result, AuthErr> { let time_changed = Utc::now(); self.store - .add_episode_actions(self.user, actions, time_changed)?; + .add_episode_actions(user, actions, time_changed)?; Ok(time_changed + TimeDelta::seconds(1)) } - /// Get episode actions for the currently authenticated user pub fn episode_actions_for_user( &self, + user: &models::User, since: Option>, podcast: Option, device: Option, @@ -215,7 +260,7 @@ impl<'a> AuthenticatedRepository<'a> { let now = chrono::Utc::now(); let actions = self .store - .episode_actions_for_user(self.user, since, podcast, device, aggregated)?; + .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/gpodder/src/repository/admin.rs b/gpodder/src/repository/admin.rs deleted file mode 100644 index 8a3e845..0000000 --- a/gpodder/src/repository/admin.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::{AuthErr, Page, models}; - -/// Admin view of the repository, providing methods only allowed by admins -pub struct AdminRepository<'a> { - pub(crate) store: &'a (dyn super::GpodderStore + Send + Sync), - pub(crate) user: &'a models::User, -} - -impl<'a> AdminRepository<'a> { - pub fn paginated_users(&self, page: Page) -> Result, AuthErr> { - self.store.paginated_users(page) - } -} diff --git a/gpodder/src/repository/mod.rs b/gpodder/src/repository/mod.rs deleted file mode 100644 index ef87237..0000000 --- a/gpodder/src/repository/mod.rs +++ /dev/null @@ -1,120 +0,0 @@ -mod admin; -mod authenticated; - -use std::sync::Arc; - -use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString}; -use chrono::{TimeDelta, Utc}; -use rand::rngs::OsRng; - -use crate::{ - models, - store::{AuthErr, GpodderStore}, -}; - -const MAX_SESSION_AGE: TimeDelta = TimeDelta::seconds(60 * 60 * 24 * 7); - -/// Main abstraction over the database that provides API-compatible methods for querying and -/// modifying the underlying database -#[derive(Clone)] -pub struct GpodderRepository { - store: Arc, -} - -impl GpodderRepository { - pub fn new(store: impl GpodderStore + Send + Sync + 'static) -> Self { - Self { - store: Arc::new(store), - } - } - - /// Return an authenticated view of the repository for the given user - pub fn user<'a>( - &'a self, - user: &'a models::User, - ) -> authenticated::AuthenticatedRepository<'a> { - authenticated::AuthenticatedRepository { - store: self.store.as_ref(), - user, - } - } - - /// Return an admin view of the repository, if the user is an admin - pub fn admin<'a>( - &'a self, - user: &'a models::User, - ) -> Result, AuthErr> { - if user.admin { - Ok(admin::AdminRepository { - store: self.store.as_ref(), - user, - }) - } else { - Err(AuthErr::NotAnAdmin) - } - } - - pub fn get_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 > MAX_SESSION_AGE { - Err(AuthErr::UnknownSession) - } else { - Ok(session) - } - } - - pub fn get_user(&self, username: &str) -> Result { - self.store.get_user(username)?.ok_or(AuthErr::UnknownUser) - } - - pub fn create_user(&self, username: &str, password: &str) -> Result { - let salt = SaltString::generate(&mut OsRng); - - let password_hash = Argon2::default() - .hash_password(password.as_bytes(), &salt) - .unwrap() - .to_string(); - - self.store.insert_user(username, &password_hash) - } - - 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 refresh_session(&self, session: &models::Session) -> Result<(), AuthErr> { - let now = Utc::now(); - - self.store.refresh_session(session, now) - } - - pub fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> { - self.store.remove_session(session_id) - } - - pub fn remove_old_sessions(&self) -> Result { - let min_last_seen = Utc::now() - MAX_SESSION_AGE; - - self.store.remove_old_sessions(min_last_seen) - } -} diff --git a/gpodder/src/store.rs b/gpodder/src/store.rs index 1daa3c2..4b60b24 100644 --- a/gpodder/src/store.rs +++ b/gpodder/src/store.rs @@ -8,7 +8,6 @@ pub enum AuthErr { UnknownSession, UnknownUser, InvalidPassword, - NotAnAdmin, Other(Box), } @@ -18,7 +17,6 @@ impl Display for AuthErr { Self::UnknownUser => write!(f, "unknown user"), Self::UnknownSession => write!(f, "unknown session"), Self::InvalidPassword => write!(f, "invalid password"), - Self::NotAnAdmin => write!(f, "not an admin"), Self::Other(err) => err.fmt(f), } } @@ -64,9 +62,6 @@ pub trait GpodderAuthStore { /// Remove any sessions whose last_seen timestamp is before the given minimum value fn remove_old_sessions(&self, min_last_seen: DateTime) -> Result; - - /// Return the given page of users, ordered by username - fn paginated_users(&self, page: Page) -> Result, AuthErr>; } pub trait GpodderDeviceStore { diff --git a/gpodder_sqlite/migrations/2025-06-24-110307_user_admin_field/down.sql b/gpodder_sqlite/migrations/2025-06-24-110307_user_admin_field/down.sql deleted file mode 100644 index 40a39a7..0000000 --- a/gpodder_sqlite/migrations/2025-06-24-110307_user_admin_field/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table users - drop column admin; diff --git a/gpodder_sqlite/migrations/2025-06-24-110307_user_admin_field/up.sql b/gpodder_sqlite/migrations/2025-06-24-110307_user_admin_field/up.sql deleted file mode 100644 index 3a04ac9..0000000 --- a/gpodder_sqlite/migrations/2025-06-24-110307_user_admin_field/up.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table users - add column admin boolean not null default false; diff --git a/gpodder_sqlite/src/models/user.rs b/gpodder_sqlite/src/models/user.rs index d0222ec..72ea62b 100644 --- a/gpodder_sqlite/src/models/user.rs +++ b/gpodder_sqlite/src/models/user.rs @@ -9,7 +9,6 @@ pub struct User { pub id: i64, pub username: String, pub password_hash: String, - pub admin: bool, } #[derive(Insertable)] diff --git a/gpodder_sqlite/src/repository/auth.rs b/gpodder_sqlite/src/repository/auth.rs index ced2606..e32ff85 100644 --- a/gpodder_sqlite/src/repository/auth.rs +++ b/gpodder_sqlite/src/repository/auth.rs @@ -18,7 +18,6 @@ impl From for gpodder::User { id: value.id, username: value.username, password_hash: value.password_hash, - admin: value.admin, } } } @@ -142,17 +141,4 @@ impl gpodder::GpodderAuthStore for SqliteRepository { })() .map_err(AuthErr::from) } - - fn paginated_users(&self, page: gpodder::Page) -> Result, AuthErr> { - Ok(users::table - .select(User::as_select()) - .order(users::username.asc()) - .offset((page.page * page.per_page) as i64) - .limit(page.per_page as i64) - .get_results(&mut self.pool.get().map_err(DbError::from)?) - .map_err(DbError::from)? - .into_iter() - .map(gpodder::User::from) - .collect()) - } } diff --git a/gpodder_sqlite/src/schema.rs b/gpodder_sqlite/src/schema.rs index 438a63d..990293d 100644 --- a/gpodder_sqlite/src/schema.rs +++ b/gpodder_sqlite/src/schema.rs @@ -58,7 +58,6 @@ diesel::table! { id -> BigInt, username -> Text, password_hash -> Text, - admin -> Bool, } } diff --git a/otter/src/cli/gpo.rs b/otter/src/cli/gpo.rs index f2d0cf0..1cc2f65 100644 --- a/otter/src/cli/gpo.rs +++ b/otter/src/cli/gpo.rs @@ -23,14 +23,15 @@ impl Command { match self { Self::Sync { username, devices } => { let user = store.get_user(username)?; - store.user(&user).update_device_sync_status( + store.update_device_sync_status( + &user, vec![devices.iter().map(|s| s.as_ref()).collect()], Vec::new(), )?; } Self::Devices { username } => { let user = store.get_user(username)?; - let devices = store.user(&user).devices()?; + let devices = store.devices_for_user(&user)?; for device in devices { println!("{} ({} subscriptions)", device.id, device.subscriptions); diff --git a/otter/src/server/gpodder/advanced/auth.rs b/otter/src/server/gpodder/advanced/auth.rs index 7e1c8fc..9c920dc 100644 --- a/otter/src/server/gpodder/advanced/auth.rs +++ b/otter/src/server/gpodder/advanced/auth.rs @@ -66,7 +66,7 @@ async fn post_login( .validate_credentials(auth.username(), auth.password())?; let user_agent = user_agent.map(|header| header.to_string()); - let session = ctx.store.user(&user).create_session(user_agent)?; + let session = ctx.store.create_session(&user, user_agent)?; Ok::<_, AuthErr>(session) }) diff --git a/otter/src/server/gpodder/advanced/devices.rs b/otter/src/server/gpodder/advanced/devices.rs index e1bbc12..30bec37 100644 --- a/otter/src/server/gpodder/advanced/devices.rs +++ b/otter/src/server/gpodder/advanced/devices.rs @@ -39,7 +39,7 @@ async fn get_devices( } Ok( - tokio::task::spawn_blocking(move || ctx.store.user(&user).devices()) + tokio::task::spawn_blocking(move || ctx.store.devices_for_user(&user)) .await .unwrap() .map(|devices| Json(devices.into_iter().map(models::Device::from).collect()))?, @@ -56,11 +56,9 @@ async fn post_device( return Err(AppError::NotFound); } - tokio::task::spawn_blocking(move || { - ctx.store.user(&user).update_device_info(&id, patch.into()) - }) - .await - .unwrap()?; + tokio::task::spawn_blocking(move || ctx.store.update_device_info(&user, &id, patch.into())) + .await + .unwrap()?; Ok(()) } diff --git a/otter/src/server/gpodder/advanced/episodes.rs b/otter/src/server/gpodder/advanced/episodes.rs index 19e4e1d..2d62333 100644 --- a/otter/src/server/gpodder/advanced/episodes.rs +++ b/otter/src/server/gpodder/advanced/episodes.rs @@ -46,8 +46,7 @@ async fn post_episode_actions( Ok(tokio::task::spawn_blocking(move || { ctx.store - .user(&user) - .add_episode_actions(actions.into_iter().map(Into::into).collect()) + .add_episode_actions(&user, actions.into_iter().map(Into::into).collect()) }) .await .unwrap() @@ -91,7 +90,8 @@ async fn get_episode_actions( let since = filter.since.and_then(|ts| DateTime::from_timestamp(ts, 0)); Ok(tokio::task::spawn_blocking(move || { - ctx.store.user(&user).episode_actions_for_user( + ctx.store.episode_actions_for_user( + &user, since, filter.podcast, filter.device, diff --git a/otter/src/server/gpodder/advanced/subscriptions.rs b/otter/src/server/gpodder/advanced/subscriptions.rs index 6687d32..b430b3e 100644 --- a/otter/src/server/gpodder/advanced/subscriptions.rs +++ b/otter/src/server/gpodder/advanced/subscriptions.rs @@ -44,8 +44,7 @@ pub async fn post_subscription_changes( Ok(tokio::task::spawn_blocking(move || { ctx.store - .user(&user) - .update_subscriptions_for_device(&id, delta.add, delta.remove) + .update_subscriptions_for_device(&user, &id, delta.add, delta.remove) }) .await .unwrap() @@ -80,9 +79,7 @@ pub async fn get_subscription_changes( let since = chrono::DateTime::from_timestamp(query.since, 0).ok_or(AppError::BadRequest)?; Ok(tokio::task::spawn_blocking(move || { - ctx.store - .user(&user) - .subscription_updates_for_device(&id, since) + ctx.store.subscription_updates_for_device(&user, &id, since) }) .await .unwrap() diff --git a/otter/src/server/gpodder/advanced/sync.rs b/otter/src/server/gpodder/advanced/sync.rs index 6ba3ea2..1ac2c33 100644 --- a/otter/src/server/gpodder/advanced/sync.rs +++ b/otter/src/server/gpodder/advanced/sync.rs @@ -41,7 +41,7 @@ pub async fn get_sync_status( } Ok( - tokio::task::spawn_blocking(move || ctx.store.user(&user).devices_by_sync_group()) + tokio::task::spawn_blocking(move || ctx.store.devices_by_sync_group(&user)) .await .unwrap() .map(|(not_synchronized, synchronized)| { @@ -68,7 +68,8 @@ pub async fn post_sync_status_changes( } Ok(tokio::task::spawn_blocking(move || { - ctx.store.user(&user).update_device_sync_status( + ctx.store.update_device_sync_status( + &user, delta .synchronize .iter() @@ -77,7 +78,7 @@ pub async fn post_sync_status_changes( delta.stop_synchronize.iter().map(|s| s.as_ref()).collect(), )?; - ctx.store.user(&user).devices_by_sync_group() + ctx.store.devices_by_sync_group(&user) }) .await .unwrap() diff --git a/otter/src/server/gpodder/mod.rs b/otter/src/server/gpodder/mod.rs index 69e86f2..e8f77e3 100644 --- a/otter/src/server/gpodder/mod.rs +++ b/otter/src/server/gpodder/mod.rs @@ -123,8 +123,7 @@ impl From for AppError { match value { gpodder::AuthErr::UnknownUser | gpodder::AuthErr::UnknownSession - | gpodder::AuthErr::InvalidPassword - | gpodder::AuthErr::NotAnAdmin => Self::Unauthorized, + | gpodder::AuthErr::InvalidPassword => Self::Unauthorized, gpodder::AuthErr::Other(err) => Self::Other(err), } } diff --git a/otter/src/server/gpodder/simple/subscriptions.rs b/otter/src/server/gpodder/simple/subscriptions.rs index 404e242..cd960f1 100644 --- a/otter/src/server/gpodder/simple/subscriptions.rs +++ b/otter/src/server/gpodder/simple/subscriptions.rs @@ -34,7 +34,7 @@ pub async fn get_device_subscriptions( } Ok( - tokio::task::spawn_blocking(move || ctx.store.user(&user).subscriptions_for_device(&id)) + tokio::task::spawn_blocking(move || ctx.store.subscriptions_for_device(&user, &id)) .await .unwrap() .map(|subs| Json(subs.into_iter().map(|s| s.url).collect()))?, @@ -51,7 +51,7 @@ pub async fn get_user_subscriptions( } Ok( - tokio::task::spawn_blocking(move || ctx.store.user(&user).subscriptions()) + tokio::task::spawn_blocking(move || ctx.store.subscriptions_for_user(&user)) .await .unwrap() .map(|subs| Json(subs.into_iter().map(|s| s.url).collect()))?, @@ -69,9 +69,7 @@ pub async fn put_device_subscriptions( } Ok(tokio::task::spawn_blocking(move || { - ctx.store - .user(&user) - .set_subscriptions_for_device(&id, urls) + ctx.store.set_subscriptions_for_device(&user, &id, urls) }) .await .unwrap() diff --git a/otter/src/server/web/mod.rs b/otter/src/server/web/mod.rs index e9c5cb2..24da8ca 100644 --- a/otter/src/server/web/mod.rs +++ b/otter/src/server/web/mod.rs @@ -1,5 +1,4 @@ mod sessions; -mod users; use axum::{ Form, RequestExt, Router, @@ -77,7 +76,6 @@ pub fn router(ctx: Context) -> Router { .route("/login", get(get_login).post(post_login)) .route("/logout", post(post_logout)) .merge(sessions::router(ctx.clone())) - .merge(users::router(ctx.clone())) } async fn get_index( @@ -127,7 +125,7 @@ async fn post_login( .validate_credentials(&login.username, &login.password)?; let user_agent = user_agent.map(|header| header.to_string()); - let session = ctx.store.user(&user).create_session(user_agent)?; + let session = ctx.store.create_session(&user, user_agent)?; Ok::<_, AuthErr>(session) }) diff --git a/otter/src/server/web/sessions.rs b/otter/src/server/web/sessions.rs index 8c25f30..d05e3a7 100644 --- a/otter/src/server/web/sessions.rs +++ b/otter/src/server/web/sessions.rs @@ -31,9 +31,7 @@ pub async fn get_sessions( ) -> AppResult>> { let next_page = page.next_page(); let sessions = tokio::task::spawn_blocking(move || { - ctx.store - .user(&session.user) - .paginated_sessions(page.into()) + ctx.store.paginated_sessions(&session.user, page.into()) }) .await .unwrap()?; diff --git a/otter/src/server/web/users.rs b/otter/src/server/web/users.rs deleted file mode 100644 index f5d3467..0000000 --- a/otter/src/server/web/users.rs +++ /dev/null @@ -1,47 +0,0 @@ -use axum::{ - Extension, Router, - extract::{Path, Query, State}, - http::HeaderMap, - routing::{delete, get}, -}; - -use crate::{ - server::{ - Context, - error::{AppError, AppResult}, - }, - web::{Page, TemplateExt, TemplateResponse, ToQuery, View}, -}; - -pub fn router(ctx: Context) -> Router { - Router::new() - .route("/users", get(get_users)) - .route_layer(axum::middleware::from_fn_with_state( - ctx.clone(), - super::auth_web_middleware, - )) -} - -pub async fn get_users( - State(ctx): State, - headers: HeaderMap, - Extension(session): Extension, - Query(page): Query, -) -> AppResult>> { - let next_page = page.next_page(); - let user_id = session.user.id; - let users = tokio::task::spawn_blocking(move || { - ctx.store.admin(&session.user)?.paginated_users(page.into()) - }) - .await - .unwrap()?; - - let next_page_query = - (users.len() == next_page.per_page as usize).then_some(next_page.to_query()); - - Ok(View::Users(users, user_id, next_page_query) - .page(&headers) - .headers(&headers) - .authenticated(true) - .response(&ctx.tera)) -} diff --git a/otter/src/web/mod.rs b/otter/src/web/mod.rs index d151186..93a7119 100644 --- a/otter/src/web/mod.rs +++ b/otter/src/web/mod.rs @@ -85,10 +85,6 @@ pub fn initialize_tera() -> tera::Result { View::Sessions(Vec::new(), 0, None).template(), include_str!("templates/views/sessions.html"), ), - ( - View::Users(Vec::new(), 0, None).template(), - include_str!("templates/views/users.html"), - ), ])?; Ok(tera) diff --git a/otter/src/web/templates/views/users.html b/otter/src/web/templates/views/users.html deleted file mode 100644 index f53f815..0000000 --- a/otter/src/web/templates/views/users.html +++ /dev/null @@ -1,21 +0,0 @@ -

Users

- - - - - - - {% for user in users %} - - {% endfor %} - - {%- if next_page_query %} - - {% endif %} - -
Username
{{ user.username }}
diff --git a/otter/src/web/view.rs b/otter/src/web/view.rs index 689f3eb..b7e45ec 100644 --- a/otter/src/web/view.rs +++ b/otter/src/web/view.rs @@ -7,7 +7,6 @@ pub enum View { Index, Login, Sessions(Vec, i64, Option), - Users(Vec, i64, Option), } #[derive(Serialize)] @@ -17,19 +16,12 @@ struct Session { last_seen: DateTime, } -#[derive(Serialize)] -struct User { - id: i64, - username: String, -} - impl Template for View { fn template(&self) -> &'static str { match self { Self::Index => "views/index.html", Self::Login => "views/login.html", Self::Sessions(..) => "views/sessions.html", - Self::Users(..) => "views/users.html", } } @@ -49,18 +41,6 @@ impl Template for View { ctx.insert("next_page_query", &query.encode()); } } - Self::Users(users, current_user_id, query) => { - ctx.insert( - "users", - &users.into_iter().map(User::from).collect::>(), - ); - - ctx.insert("current_user_id", ¤t_user_id); - - if let Some(query) = query { - ctx.insert("next_page_query", &query.encode()); - } - } _ => {} }; @@ -77,12 +57,3 @@ impl From for Session { } } } - -impl From for User { - fn from(value: gpodder::User) -> Self { - Self { - id: value.id, - username: value.username, - } - } -}