diff --git a/gpodder/src/models.rs b/gpodder/src/models.rs index c37f97f..9240af8 100644 --- a/gpodder/src/models.rs +++ b/gpodder/src/models.rs @@ -77,3 +77,9 @@ pub struct Page { pub struct UserFilter { pub username: Option, } + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignupLink { + pub id: i64, + pub time_created: DateTime, +} diff --git a/gpodder/src/repository/admin.rs b/gpodder/src/repository/admin.rs index 2123026..495d982 100644 --- a/gpodder/src/repository/admin.rs +++ b/gpodder/src/repository/admin.rs @@ -1,9 +1,12 @@ +use chrono::Utc; +use rand::Rng; + 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, + pub(crate) _user: &'a models::User, } impl<'a> AdminRepository<'a> { @@ -14,4 +17,21 @@ impl<'a> AdminRepository<'a> { ) -> Result, AuthErr> { self.store.paginated_users(page, filter) } + + /// Generate a new unique signup link ID + pub fn generate_signup_link(&self) -> Result { + let link = models::SignupLink { + id: rand::thread_rng().r#gen(), + time_created: Utc::now(), + }; + + self.store.insert_signup_link(&link)?; + + Ok(link) + } + + /// Remove the signup link with the given ID, if it exists + pub fn remove_signup_link(&self, id: i64) -> Result { + self.store.remove_signup_link(id) + } } diff --git a/gpodder/src/repository/mod.rs b/gpodder/src/repository/mod.rs index ef87237..f850a05 100644 --- a/gpodder/src/repository/mod.rs +++ b/gpodder/src/repository/mod.rs @@ -47,7 +47,7 @@ impl GpodderRepository { if user.admin { Ok(admin::AdminRepository { store: self.store.as_ref(), - user, + _user: user, }) } else { Err(AuthErr::NotAnAdmin) diff --git a/gpodder/src/store.rs b/gpodder/src/store.rs index 7f32374..598fb41 100644 --- a/gpodder/src/store.rs +++ b/gpodder/src/store.rs @@ -67,6 +67,35 @@ pub trait GpodderAuthStore { /// Return the given page of users, ordered by username fn paginated_users(&self, page: Page, filter: &UserFilter) -> Result, AuthErr>; + + /// Insert the signup link into the database. + /// + /// # Errors + /// + /// If a database failure occurs + fn insert_signup_link(&self, link: &SignupLink) -> Result<(), AuthErr>; + + /// Get the signup link associated with the given ID + /// + /// # Returns + /// + /// Some(link) if the ID corresponds to an existing signup link; None otherwise + /// + /// # Errors + /// + /// If a database failure occurs + fn get_signup_link(&self, id: i64) -> Result, AuthErr>; + + /// Remove the signup link with the given ID. + /// + /// # Returns + /// + /// True if the ID existed in the database; false otherwise + /// + /// # Errors + /// + /// If a database failure occurs + fn remove_signup_link(&self, id: i64) -> Result; } pub trait GpodderDeviceStore { diff --git a/gpodder_sqlite/Cargo.toml b/gpodder_sqlite/Cargo.toml index f14c90e..75620b8 100644 --- a/gpodder_sqlite/Cargo.toml +++ b/gpodder_sqlite/Cargo.toml @@ -15,8 +15,15 @@ tracing = { workspace = true } chrono = { workspace = true, features = ["serde"] } libsqlite3-sys = { version = "0.31.0", features = ["bundled"] } -diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] } diesel_migrations = { version = "2.2.0", features = ["sqlite"] } +[dependencies.diesel] +version = "2.2.7" +features = [ + "r2d2", + "sqlite", + "returning_clauses_for_sqlite_3_35", +] + [dev-dependencies] criterion = "0.5.1" diff --git a/gpodder_sqlite/migrations/2025-07-02-080351_signup_links/down.sql b/gpodder_sqlite/migrations/2025-07-02-080351_signup_links/down.sql new file mode 100644 index 0000000..e85c318 --- /dev/null +++ b/gpodder_sqlite/migrations/2025-07-02-080351_signup_links/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +drop table signup_links; diff --git a/gpodder_sqlite/migrations/2025-07-02-080351_signup_links/up.sql b/gpodder_sqlite/migrations/2025-07-02-080351_signup_links/up.sql new file mode 100644 index 0000000..0b31f53 --- /dev/null +++ b/gpodder_sqlite/migrations/2025-07-02-080351_signup_links/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +create table signup_links ( + id bigint primary key not null, + created_at bigint not null +); diff --git a/gpodder_sqlite/src/models/mod.rs b/gpodder_sqlite/src/models/mod.rs index 8e6ebf9..7fe7f2c 100644 --- a/gpodder_sqlite/src/models/mod.rs +++ b/gpodder_sqlite/src/models/mod.rs @@ -2,5 +2,6 @@ pub mod device; pub mod device_subscription; pub mod episode_action; pub mod session; +pub mod signup_link; pub mod sync_group; pub mod user; diff --git a/gpodder_sqlite/src/models/signup_link.rs b/gpodder_sqlite/src/models/signup_link.rs new file mode 100644 index 0000000..7f68b07 --- /dev/null +++ b/gpodder_sqlite/src/models/signup_link.rs @@ -0,0 +1,11 @@ +use diesel::prelude::*; + +use crate::schema::*; + +#[derive(Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = signup_links)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct SignupLink { + pub id: i64, + pub created_at: i64, +} diff --git a/gpodder_sqlite/src/repository/auth.rs b/gpodder_sqlite/src/repository/auth.rs index 38af31e..1988a4c 100644 --- a/gpodder_sqlite/src/repository/auth.rs +++ b/gpodder_sqlite/src/repository/auth.rs @@ -7,6 +7,7 @@ use crate::{ DbError, models::{ session::Session, + signup_link::SignupLink, user::{NewUser, User}, }, schema::*, @@ -23,6 +24,15 @@ impl From for gpodder::User { } } +impl From for gpodder::SignupLink { + fn from(value: SignupLink) -> Self { + Self { + id: value.id, + time_created: DateTime::from_timestamp(value.created_at, 0).unwrap(), + } + } +} + impl gpodder::GpodderAuthStore for SqliteRepository { fn get_user(&self, username: &str) -> Result, AuthErr> { Ok(users::table @@ -170,4 +180,39 @@ impl gpodder::GpodderAuthStore for SqliteRepository { })() .map_err(AuthErr::from) } + + fn get_signup_link(&self, id: i64) -> Result, AuthErr> { + match signup_links::table + .find(id) + .select(SignupLink::as_select()) + .first(&mut self.pool.get().map_err(DbError::from)?) + .optional() + { + Ok(Some(link)) => Ok(Some(gpodder::SignupLink::from(link))), + Ok(None) => Ok(None), + Err(err) => Err(DbError::from(err).into()), + } + } + + fn insert_signup_link(&self, link: &gpodder::SignupLink) -> Result<(), AuthErr> { + diesel::insert_into(signup_links::table) + .values(SignupLink { + id: link.id, + created_at: link.time_created.timestamp(), + }) + .execute(&mut self.pool.get().map_err(DbError::from)?) + .map_err(DbError::from)?; + + Ok(()) + } + + fn remove_signup_link(&self, id: i64) -> Result { + match diesel::delete(signup_links::table.filter(signup_links::id.eq(id))) + .execute(&mut self.pool.get().map_err(DbError::from)?) + { + Ok(0) => Ok(false), + Ok(_) => Ok(true), + Err(err) => Err(DbError::from(err).into()), + } + } } diff --git a/gpodder_sqlite/src/schema.rs b/gpodder_sqlite/src/schema.rs index 438a63d..f8402ed 100644 --- a/gpodder_sqlite/src/schema.rs +++ b/gpodder_sqlite/src/schema.rs @@ -47,6 +47,13 @@ diesel::table! { } } +diesel::table! { + signup_links (id) { + id -> BigInt, + created_at -> BigInt, + } +} + diesel::table! { sync_groups (id) { id -> BigInt, @@ -74,6 +81,7 @@ diesel::allow_tables_to_appear_in_same_query!( devices, episode_actions, sessions, + signup_links, sync_groups, users, );