feat: implement simple api subscription routes
parent
caad08c99e
commit
7db6ebf213
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
data
|
data
|
||||||
|
venv
|
||||||
|
|
|
@ -3,6 +3,7 @@ mod schema;
|
||||||
|
|
||||||
pub use models::device::{Device, DeviceType, NewDevice};
|
pub use models::device::{Device, DeviceType, NewDevice};
|
||||||
pub use models::session::Session;
|
pub use models::session::Session;
|
||||||
|
pub use models::subscription::{NewSubscription, Subscription};
|
||||||
pub use models::user::{NewUser, User};
|
pub use models::user::{NewUser, User};
|
||||||
|
|
||||||
use diesel::{
|
use diesel::{
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub struct NewSubscription {
|
||||||
|
|
||||||
impl Subscription {
|
impl Subscription {
|
||||||
pub fn for_device(pool: &DbPool, device_id: i64) -> DbResult<Vec<String>> {
|
pub fn for_device(pool: &DbPool, device_id: i64) -> DbResult<Vec<String>> {
|
||||||
Ok(subscriptions::dsl::subscriptions
|
Ok(subscriptions::table
|
||||||
.select(subscriptions::url)
|
.select(subscriptions::url)
|
||||||
.filter(subscriptions::device_id.eq(device_id))
|
.filter(subscriptions::device_id.eq(device_id))
|
||||||
.get_results(&mut pool.get()?)?)
|
.get_results(&mut pool.get()?)?)
|
||||||
|
@ -36,4 +36,21 @@ impl Subscription {
|
||||||
.distinct()
|
.distinct()
|
||||||
.get_results(&mut pool.get()?)?)
|
.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(())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ use serde::{
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Format {
|
pub enum Format {
|
||||||
Json,
|
Json,
|
||||||
OPML,
|
// OPML,
|
||||||
Plaintext,
|
// Plaintext,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
@ -5,7 +5,7 @@ mod simple;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Request, State},
|
extract::{Request, State},
|
||||||
http::{HeaderName, HeaderValue, StatusCode},
|
http::{header::WWW_AUTHENTICATE, HeaderName, HeaderValue, StatusCode},
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
RequestExt, Router,
|
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 {
|
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
|
||||||
// SAFETY: this extractor's error type is Infallible
|
// SAFETY: this extractor's error type is Infallible
|
||||||
let jar: CookieJar = req.extract_parts().await.unwrap();
|
let jar: CookieJar = req.extract_parts().await.unwrap();
|
||||||
tracing::debug!("{:?}", jar);
|
|
||||||
let mut auth_user = None;
|
let mut auth_user = None;
|
||||||
let mut new_session_id = 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
|
res
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
mod subscriptions;
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
|
||||||
use crate::server::Context;
|
use crate::server::Context;
|
||||||
|
|
||||||
pub fn router(ctx: Context) -> Router<Context> {
|
pub fn router(ctx: Context) -> Router<Context> {
|
||||||
Router::new()
|
Router::new().nest("/subscriptions", subscriptions::router(ctx))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
Loading…
Reference in New Issue