feat: implement simple api subscription routes
parent
caad08c99e
commit
7db6ebf213
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
data
|
||||
venv
|
||||
|
|
|
@ -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::{
|
||||
|
|
|
@ -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(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ use serde::{
|
|||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Format {
|
||||
Json,
|
||||
OPML,
|
||||
Plaintext,
|
||||
// OPML,
|
||||
// Plaintext,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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