feat: implement simple api subscription routes

episode-actions
Jef Roosens 2025-02-24 16:00:49 +01:00
parent caad08c99e
commit 7db6ebf213
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
7 changed files with 131 additions and 7 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
data
venv

View File

@ -3,6 +3,7 @@ mod schema;
pub use models::device::{Device, DeviceType, NewDevice};
pub use models::session::Session;
pub use models::subscription::{NewSubscription, Subscription};
pub use models::user::{NewUser, User};
use diesel::{

View File

@ -22,7 +22,7 @@ pub struct NewSubscription {
impl Subscription {
pub fn for_device(pool: &DbPool, device_id: i64) -> DbResult<Vec<String>> {
Ok(subscriptions::dsl::subscriptions
Ok(subscriptions::table
.select(subscriptions::url)
.filter(subscriptions::device_id.eq(device_id))
.get_results(&mut pool.get()?)?)
@ -36,4 +36,21 @@ impl Subscription {
.distinct()
.get_results(&mut pool.get()?)?)
}
pub fn update_for_device(pool: &DbPool, device_id: i64, urls: Vec<String>) -> DbResult<()> {
pool.get()?.transaction(|conn| {
diesel::delete(subscriptions::table.filter(subscriptions::device_id.eq(device_id)))
.execute(conn)?;
diesel::insert_into(subscriptions::table)
.values(
urls.into_iter()
.map(|url| NewSubscription { device_id, url })
.collect::<Vec<_>>(),
)
.execute(conn)?;
Ok(())
})
}
}

View File

@ -9,8 +9,8 @@ use serde::{
#[serde(rename_all = "lowercase")]
pub enum Format {
Json,
OPML,
Plaintext,
// OPML,
// Plaintext,
}
#[derive(Debug)]

View File

@ -5,7 +5,7 @@ mod simple;
use axum::{
extract::{Request, State},
http::{HeaderName, HeaderValue, StatusCode},
http::{header::WWW_AUTHENTICATE, HeaderName, HeaderValue, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
RequestExt, Router,
@ -43,7 +43,6 @@ pub fn router(ctx: Context) -> Router<Context> {
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
// SAFETY: this extractor's error type is Infallible
let jar: CookieJar = req.extract_parts().await.unwrap();
tracing::debug!("{:?}", jar);
let mut auth_user = None;
let mut new_session_id = None;
@ -107,6 +106,14 @@ pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next:
res
}
} else {
StatusCode::UNAUTHORIZED.into_response()
let mut res = StatusCode::UNAUTHORIZED.into_response();
// This is what the gpodder.net service returns, and some clients seem to depend on it
res.headers_mut().insert(
WWW_AUTHENTICATE,
HeaderValue::from_static("Basic realm=\"\""),
);
res
}
}

View File

@ -1,7 +1,9 @@
mod subscriptions;
use axum::Router;
use crate::server::Context;
pub fn router(ctx: Context) -> Router<Context> {
Router::new()
Router::new().nest("/subscriptions", subscriptions::router(ctx))
}

View File

@ -0,0 +1,96 @@
use axum::{
extract::{Path, State},
middleware,
routing::get,
Extension, Form, Json, Router,
};
use crate::{
db,
server::{
error::{AppError, AppResult},
gpodder::{auth_middleware, format::StringWithFormat, models::DeviceType},
Context,
},
};
pub fn router(ctx: Context) -> Router<Context> {
Router::new()
.route(
"/{username}/{id}",
get(get_device_subscriptions).put(put_device_subscriptions),
)
.route("/{username}", get(get_user_subscriptions))
.layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
}
pub async fn get_device_subscriptions(
State(ctx): State<Context>,
Path((username, id)): Path<(String, StringWithFormat)>,
Extension(user): Extension<db::User>,
) -> AppResult<Json<Vec<String>>> {
if username != user.username {
return Err(AppError::BadRequest);
}
let subscriptions = tokio::task::spawn_blocking(move || {
let device =
db::Device::by_device_id(&ctx.pool, user.id, &id)?.ok_or(AppError::NotFound)?;
Ok::<_, AppError>(db::Subscription::for_device(&ctx.pool, device.id)?)
})
.await
.unwrap()?;
Ok(Json(subscriptions))
}
pub async fn get_user_subscriptions(
State(ctx): State<Context>,
Path(username): Path<StringWithFormat>,
Extension(user): Extension<db::User>,
) -> AppResult<Json<Vec<String>>> {
if *username != user.username {
return Err(AppError::BadRequest);
}
let subscriptions =
tokio::task::spawn_blocking(move || db::Subscription::for_user(&ctx.pool, user.id))
.await
.unwrap()?;
Ok(Json(subscriptions))
}
pub async fn put_device_subscriptions(
State(ctx): State<Context>,
Path((username, id)): Path<(String, StringWithFormat)>,
Extension(user): Extension<db::User>,
Json(urls): Json<Vec<String>>,
) -> AppResult<()> {
if *username != user.username {
return Err(AppError::BadRequest);
}
tokio::task::spawn_blocking(move || {
let device = if let Some(device) = db::Device::by_device_id(&ctx.pool, user.id, &id)? {
device
} else {
db::NewDevice::new(
user.id,
id.to_string(),
String::new(),
DeviceType::Other.into(),
)
.insert(&ctx.pool)?
};
Ok::<_, AppError>(db::Subscription::update_for_device(
&ctx.pool, device.id, urls,
)?)
})
.await
.unwrap()?;
Ok(())
}