From a2233d9da89163edbe66bb555a24c85edd9ccb74 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 27 Feb 2025 22:08:49 +0100 Subject: [PATCH] feat: migrate devices api to repository --- bruno/Device API/List devices for user.bru | 15 ++++ bruno/Device API/Update device data.bru | 20 +++++ src/db/repository/device.rs | 98 ++++++++++++++++++++++ src/db/repository/mod.rs | 1 + src/gpodder/mod.rs | 18 +++- src/gpodder/models.rs | 27 ++++++ src/server/gpodder/advanced/devices.rs | 58 ++++--------- src/server/gpodder/mod.rs | 19 ++++- 8 files changed, 206 insertions(+), 50 deletions(-) create mode 100644 bruno/Device API/List devices for user.bru create mode 100644 bruno/Device API/Update device data.bru create mode 100644 src/db/repository/device.rs diff --git a/bruno/Device API/List devices for user.bru b/bruno/Device API/List devices for user.bru new file mode 100644 index 0000000..8b03bb7 --- /dev/null +++ b/bruno/Device API/List devices for user.bru @@ -0,0 +1,15 @@ +meta { + name: List devices for user + type: http + seq: 1 +} + +get { + url: http://localhost:8080/api/2/devices/:user + body: none + auth: inherit +} + +params:path { + user: test.json +} diff --git a/bruno/Device API/Update device data.bru b/bruno/Device API/Update device data.bru new file mode 100644 index 0000000..35fec36 --- /dev/null +++ b/bruno/Device API/Update device data.bru @@ -0,0 +1,20 @@ +meta { + name: Update device data + type: http + seq: 2 +} + +post { + url: http://localhost:8080/api/2/devices/:user/:device_id + body: json + auth: inherit +} + +params:path { + user: test + device_id: test4.json +} + +body:json { + {"caption": "ello bruv"} +} diff --git a/src/db/repository/device.rs b/src/db/repository/device.rs new file mode 100644 index 0000000..18a8516 --- /dev/null +++ b/src/db/repository/device.rs @@ -0,0 +1,98 @@ +use diesel::prelude::*; + +use super::SqliteRepository; +use crate::{ + db::{self, schema::*}, + gpodder, +}; + +impl From for gpodder::DeviceType { + fn from(value: db::DeviceType) -> Self { + match value { + db::DeviceType::Desktop => Self::Desktop, + db::DeviceType::Laptop => Self::Laptop, + db::DeviceType::Mobile => Self::Mobile, + db::DeviceType::Server => Self::Server, + db::DeviceType::Other => Self::Other, + } + } +} + +impl From for db::DeviceType { + fn from(value: gpodder::DeviceType) -> Self { + match value { + gpodder::DeviceType::Desktop => Self::Desktop, + gpodder::DeviceType::Laptop => Self::Laptop, + gpodder::DeviceType::Mobile => Self::Mobile, + gpodder::DeviceType::Server => Self::Server, + gpodder::DeviceType::Other => Self::Other, + } + } +} + +impl gpodder::DeviceRepository for SqliteRepository { + fn devices_for_user( + &self, + user: &gpodder::User, + ) -> Result, gpodder::AuthErr> { + Ok(devices::table + .select(db::Device::as_select()) + .filter(devices::user_id.eq(user.id)) + .get_results(&mut self.pool.get()?)? + .into_iter() + .map(|d| gpodder::Device { + id: d.device_id, + caption: d.caption, + r#type: d.type_.into(), + // TODO implement subscription count + subscriptions: 0, + }) + .collect()) + } + + fn update_device_info( + &self, + user: &gpodder::User, + device_id: &str, + patch: gpodder::DevicePatch, + ) -> Result<(), gpodder::AuthErr> { + if let Some(mut device) = devices::table + .select(db::Device::as_select()) + .filter( + devices::user_id + .eq(user.id) + .and(devices::device_id.eq(device_id)), + ) + .get_result(&mut self.pool.get()?) + .optional()? + { + if let Some(caption) = patch.caption { + device.caption = caption; + } + + if let Some(type_) = patch.r#type { + device.type_ = type_.into(); + } + + diesel::update(devices::table.filter(devices::id.eq(device.id))) + .set(( + devices::caption.eq(&device.caption), + devices::type_.eq(&device.type_), + )) + .execute(&mut self.pool.get()?)?; + } else { + let device = db::NewDevice { + device_id: device_id.to_string(), + user_id: user.id, + caption: patch.caption.unwrap_or(String::new()), + type_: patch.r#type.unwrap_or(gpodder::DeviceType::Other).into(), + }; + + diesel::insert_into(devices::table) + .values(device) + .execute(&mut self.pool.get()?)?; + } + + Ok(()) + } +} diff --git a/src/db/repository/mod.rs b/src/db/repository/mod.rs index 84d2415..dd53e86 100644 --- a/src/db/repository/mod.rs +++ b/src/db/repository/mod.rs @@ -1,4 +1,5 @@ mod auth; +mod device; use super::DbPool; diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index 6e5e702..205bb69 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -6,7 +6,7 @@ pub enum AuthErr { UnknownSession, UnknownUser, InvalidPassword, - Other(Box), + Other(Box), } pub trait AuthRepository: Send + Sync { @@ -21,6 +21,16 @@ pub trait AuthRepository: Send + Sync { ) -> Result<(i64, models::User), AuthErr>; } -// pub trait DeviceRepository: Send + Sync { -// fn devices_for_user(&self, ) -// } +pub trait DeviceRepository: Send + Sync { + /// Return all devices associated with the user + 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. + fn update_device_info( + &self, + user: &User, + device: &str, + patch: DevicePatch, + ) -> Result<(), AuthErr>; +} diff --git a/src/gpodder/models.rs b/src/gpodder/models.rs index 764b33f..296d612 100644 --- a/src/gpodder/models.rs +++ b/src/gpodder/models.rs @@ -1,4 +1,31 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] pub struct User { pub id: i64, pub username: 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, +} diff --git a/src/server/gpodder/advanced/devices.rs b/src/server/gpodder/advanced/devices.rs index 7df6ec1..bb90dc2 100644 --- a/src/server/gpodder/advanced/devices.rs +++ b/src/server/gpodder/advanced/devices.rs @@ -6,13 +6,12 @@ use axum::{ }; use crate::{ - db, + gpodder::{self, DeviceRepository}, server::{ error::{AppError, AppResult}, gpodder::{ auth_middleware, format::{Format, StringWithFormat}, - models::{Device, DevicePatch, DeviceType}, }, Context, }, @@ -28,8 +27,8 @@ pub fn router(ctx: Context) -> Router { async fn get_devices( State(ctx): State, Path(username): Path, - Extension(user): Extension, -) -> AppResult>> { + Extension(user): Extension, +) -> AppResult>> { if username.format != Format::Json { return Err(AppError::NotFound); } @@ -38,56 +37,27 @@ async fn get_devices( return Err(AppError::BadRequest); } - let devices = tokio::task::spawn_blocking(move || db::Device::for_user(&ctx.pool, user.id)) - .await - .unwrap()? - .into_iter() - .map(|d| Device { - id: d.device_id, - caption: d.caption, - r#type: d.type_.into(), - // TODO implement subscription count - subscriptions: 0, - }) - .collect(); - - Ok(Json(devices)) + Ok( + tokio::task::spawn_blocking(move || ctx.repo.devices_for_user(&user)) + .await + .unwrap() + .map(Json)?, + ) } async fn post_device( State(ctx): State, Path((_username, id)): Path<(String, StringWithFormat)>, - Extension(user): Extension, - Json(patch): Json, + Extension(user): Extension, + Json(patch): Json, ) -> AppResult<()> { if id.format != Format::Json { return Err(AppError::NotFound); } - tokio::task::spawn_blocking(move || { - if let Some(mut device) = db::Device::by_device_id(&ctx.pool, user.id, &id)? { - if let Some(caption) = patch.caption { - device.caption = caption; - } - - if let Some(type_) = patch.r#type { - device.type_ = type_.into(); - } - - device.update(&ctx.pool) - } else { - db::NewDevice::new( - user.id, - id.to_string(), - patch.caption.unwrap_or(String::new()), - patch.r#type.unwrap_or(DeviceType::Other).into(), - ) - .insert(&ctx.pool) - .map(|_| ()) - } - }) - .await - .unwrap()?; + tokio::task::spawn_blocking(move || ctx.repo.update_device_info(&user, &id, patch)) + .await + .unwrap()?; Ok(()) } diff --git a/src/server/gpodder/mod.rs b/src/server/gpodder/mod.rs index 1c8dd50..6cc3c8e 100644 --- a/src/server/gpodder/mod.rs +++ b/src/server/gpodder/mod.rs @@ -20,7 +20,7 @@ use axum_extra::{ }; use tower_http::set_header::SetResponseHeaderLayer; -use crate::{db, server::error::AppError}; +use crate::{db, gpodder, server::error::AppError}; use super::Context; @@ -90,7 +90,11 @@ pub async fn auth_middleware(State(ctx): State, mut req: Request, next: } if let Some(user) = auth_user { - req.extensions_mut().insert(user); + req.extensions_mut().insert(user.clone()); + req.extensions_mut().insert(gpodder::User { + username: user.username, + id: user.id, + }); let res = next.run(req).await; if let Some(session_id) = new_session_id { @@ -117,3 +121,14 @@ pub async fn auth_middleware(State(ctx): State, mut req: Request, next: res } } + +impl From for AppError { + fn from(value: gpodder::AuthErr) -> Self { + match value { + gpodder::AuthErr::UnknownUser + | gpodder::AuthErr::UnknownSession + | gpodder::AuthErr::InvalidPassword => Self::Unauthorized, + gpodder::AuthErr::Other(err) => Self::Other(err), + } + } +}