feat: migrate devices api to repository

episode-actions
Jef Roosens 2025-02-27 22:08:49 +01:00
parent 952f92c178
commit a2233d9da8
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
8 changed files with 206 additions and 50 deletions

View File

@ -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
}

View File

@ -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"}
}

View File

@ -0,0 +1,98 @@
use diesel::prelude::*;
use super::SqliteRepository;
use crate::{
db::{self, schema::*},
gpodder,
};
impl From<db::DeviceType> 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<gpodder::DeviceType> 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<Vec<gpodder::Device>, 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(())
}
}

View File

@ -1,4 +1,5 @@
mod auth; mod auth;
mod device;
use super::DbPool; use super::DbPool;

View File

@ -6,7 +6,7 @@ pub enum AuthErr {
UnknownSession, UnknownSession,
UnknownUser, UnknownUser,
InvalidPassword, InvalidPassword,
Other(Box<dyn std::error::Error>), Other(Box<dyn std::error::Error + Sync + Send>),
} }
pub trait AuthRepository: Send + Sync { pub trait AuthRepository: Send + Sync {
@ -21,6 +21,16 @@ pub trait AuthRepository: Send + Sync {
) -> Result<(i64, models::User), AuthErr>; ) -> Result<(i64, models::User), AuthErr>;
} }
// pub trait DeviceRepository: Send + Sync { pub trait DeviceRepository: Send + Sync {
// fn devices_for_user(&self, ) /// Return all devices associated with the user
// } fn devices_for_user(&self, user: &User) -> Result<Vec<Device>, 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>;
}

View File

@ -1,4 +1,31 @@
use serde::{Deserialize, Serialize};
#[derive(Clone)]
pub struct User { pub struct User {
pub id: i64, pub id: i64,
pub username: String, 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<String>,
pub r#type: Option<DeviceType>,
}

View File

@ -6,13 +6,12 @@ use axum::{
}; };
use crate::{ use crate::{
db, gpodder::{self, DeviceRepository},
server::{ server::{
error::{AppError, AppResult}, error::{AppError, AppResult},
gpodder::{ gpodder::{
auth_middleware, auth_middleware,
format::{Format, StringWithFormat}, format::{Format, StringWithFormat},
models::{Device, DevicePatch, DeviceType},
}, },
Context, Context,
}, },
@ -28,8 +27,8 @@ pub fn router(ctx: Context) -> Router<Context> {
async fn get_devices( async fn get_devices(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(username): Path<StringWithFormat>, Path(username): Path<StringWithFormat>,
Extension(user): Extension<db::User>, Extension(user): Extension<gpodder::User>,
) -> AppResult<Json<Vec<Device>>> { ) -> AppResult<Json<Vec<gpodder::Device>>> {
if username.format != Format::Json { if username.format != Format::Json {
return Err(AppError::NotFound); return Err(AppError::NotFound);
} }
@ -38,54 +37,25 @@ async fn get_devices(
return Err(AppError::BadRequest); return Err(AppError::BadRequest);
} }
let devices = tokio::task::spawn_blocking(move || db::Device::for_user(&ctx.pool, user.id)) Ok(
tokio::task::spawn_blocking(move || ctx.repo.devices_for_user(&user))
.await .await
.unwrap()? .unwrap()
.into_iter() .map(Json)?,
.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))
} }
async fn post_device( async fn post_device(
State(ctx): State<Context>, State(ctx): State<Context>,
Path((_username, id)): Path<(String, StringWithFormat)>, Path((_username, id)): Path<(String, StringWithFormat)>,
Extension(user): Extension<db::User>, Extension(user): Extension<gpodder::User>,
Json(patch): Json<DevicePatch>, Json(patch): Json<gpodder::DevicePatch>,
) -> AppResult<()> { ) -> AppResult<()> {
if id.format != Format::Json { if id.format != Format::Json {
return Err(AppError::NotFound); return Err(AppError::NotFound);
} }
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || ctx.repo.update_device_info(&user, &id, patch))
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 .await
.unwrap()?; .unwrap()?;

View File

@ -20,7 +20,7 @@ use axum_extra::{
}; };
use tower_http::set_header::SetResponseHeaderLayer; use tower_http::set_header::SetResponseHeaderLayer;
use crate::{db, server::error::AppError}; use crate::{db, gpodder, server::error::AppError};
use super::Context; use super::Context;
@ -90,7 +90,11 @@ pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next:
} }
if let Some(user) = auth_user { 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; let res = next.run(req).await;
if let Some(session_id) = new_session_id { if let Some(session_id) = new_session_id {
@ -117,3 +121,14 @@ pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next:
res res
} }
} }
impl From<gpodder::AuthErr> 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),
}
}
}