From 73928e78f491cb1755e0e6b12f9ca90052f07ec3 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 24 Feb 2025 10:42:59 +0100 Subject: [PATCH] refactor: restructure using simple and advanced api --- src/db/models/user.rs | 2 +- src/server/gpodder/{ => advanced}/auth.rs | 81 +-------------- src/server/gpodder/{ => advanced}/devices.rs | 55 +--------- src/server/gpodder/advanced/mod.rs | 12 +++ src/server/gpodder/mod.rs | 100 +++++++++++++++++-- src/server/gpodder/models.rs | 51 ++++++++++ src/server/gpodder/simple/mod.rs | 7 ++ src/server/mod.rs | 2 +- 8 files changed, 173 insertions(+), 137 deletions(-) rename src/server/gpodder/{ => advanced}/auth.rs (50%) rename src/server/gpodder/{ => advanced}/devices.rs (66%) create mode 100644 src/server/gpodder/advanced/mod.rs create mode 100644 src/server/gpodder/models.rs create mode 100644 src/server/gpodder/simple/mod.rs diff --git a/src/db/models/user.rs b/src/db/models/user.rs index f51226f..f9c9bb9 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -1,5 +1,5 @@ use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; -use diesel::{prelude::*, sqlite::Sqlite}; +use diesel::prelude::*; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; diff --git a/src/server/gpodder/auth.rs b/src/server/gpodder/advanced/auth.rs similarity index 50% rename from src/server/gpodder/auth.rs rename to src/server/gpodder/advanced/auth.rs index a304392..97fb048 100644 --- a/src/server/gpodder/auth.rs +++ b/src/server/gpodder/advanced/auth.rs @@ -1,10 +1,7 @@ use axum::{ - extract::{Path, Request, State}, - http::StatusCode, - middleware::Next, - response::{IntoResponse, Response}, + extract::{Path, State}, routing::post, - RequestExt, Router, + Router, }; use axum_extra::{ extract::{ @@ -19,12 +16,11 @@ use crate::{ db::{Session, User}, server::{ error::{AppError, AppResult}, + gpodder::SESSION_ID_COOKIE, Context, }, }; -const SESSION_ID_COOKIE: &str = "sessionid"; - pub fn router() -> Router { Router::new() .route("/{username}/login.json", post(post_login)) @@ -93,74 +89,3 @@ async fn post_logout( Ok(jar) } } - -/// This middleware accepts -pub async fn auth_middleware(State(ctx): State, 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; - - if let Some(session_id) = jar - .get(SESSION_ID_COOKIE) - .and_then(|c| c.value().parse::().ok()) - { - match tokio::task::spawn_blocking(move || Session::user_from_id(&ctx.pool, session_id)) - .await - .unwrap() - .map_err(AppError::Db) - { - Ok(user) => { - auth_user = user; - } - Err(err) => { - return err.into_response(); - } - }; - } else if let Ok(auth) = req - .extract_parts::>>() - .await - { - match tokio::task::spawn_blocking(move || { - let user = User::by_username(&ctx.pool, auth.username())?.ok_or(AppError::NotFound)?; - - if user.verify_password(auth.password()) { - Ok((Session::new_for_user(&ctx.pool, user.id)?, user)) - } else { - Err(AppError::Unauthorized) - } - }) - .await - .unwrap() - { - Ok((session, user)) => { - auth_user = Some(user); - new_session_id = Some(session.id); - } - Err(err) => { - return err.into_response(); - } - } - } - - if let Some(user) = auth_user { - req.extensions_mut().insert(user); - let res = next.run(req).await; - - if let Some(session_id) = new_session_id { - ( - jar.add( - Cookie::build((SESSION_ID_COOKIE, session_id.to_string())) - .expires(Expiration::Session), - ), - res, - ) - .into_response() - } else { - res - } - } else { - StatusCode::UNAUTHORIZED.into_response() - } -} diff --git a/src/server/gpodder/devices.rs b/src/server/gpodder/advanced/devices.rs similarity index 66% rename from src/server/gpodder/devices.rs rename to src/server/gpodder/advanced/devices.rs index d336b56..7be52ef 100644 --- a/src/server/gpodder/devices.rs +++ b/src/server/gpodder/advanced/devices.rs @@ -4,18 +4,19 @@ use axum::{ routing::{get, post}, Extension, Json, Router, }; -use serde::{Deserialize, Serialize}; use crate::{ db::{self, User}, server::{ error::{AppError, AppResult}, + gpodder::{ + auth_middleware, + models::{Device, DevicePatch, DeviceType}, + }, Context, }, }; -use super::auth::auth_middleware; - pub fn router(ctx: Context) -> Router { Router::new() .route("/{username}", get(get_devices)) @@ -23,48 +24,6 @@ pub fn router(ctx: Context) -> Router { .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware)) } -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum DeviceType { - Desktop, - Laptop, - Mobile, - Server, - Other, -} - -impl From for db::DeviceType { - fn from(value: DeviceType) -> Self { - match value { - DeviceType::Desktop => Self::Desktop, - DeviceType::Laptop => Self::Laptop, - DeviceType::Mobile => Self::Mobile, - DeviceType::Server => Self::Server, - DeviceType::Other => Self::Other, - } - } -} - -impl From for 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, - } - } -} - -#[derive(Serialize)] -pub struct Device { - id: String, - caption: String, - r#type: DeviceType, - subscriptions: i64, -} - async fn get_devices( State(ctx): State, Path(username): Path, @@ -94,12 +53,6 @@ async fn get_devices( Ok(Json(devices)) } -#[derive(Deserialize)] -pub struct DevicePatch { - caption: Option, - r#type: Option, -} - async fn post_device( State(ctx): State, Path((_username, id)): Path<(String, String)>, diff --git a/src/server/gpodder/advanced/mod.rs b/src/server/gpodder/advanced/mod.rs new file mode 100644 index 0000000..6ee8c1d --- /dev/null +++ b/src/server/gpodder/advanced/mod.rs @@ -0,0 +1,12 @@ +use axum::Router; + +use crate::server::Context; + +mod auth; +mod devices; + +pub fn router(ctx: Context) -> Router { + Router::new() + .nest("/auth", auth::router()) + .nest("/devices", devices::router(ctx.clone())) +} diff --git a/src/server/gpodder/mod.rs b/src/server/gpodder/mod.rs index 6add829..7b2ba67 100644 --- a/src/server/gpodder/mod.rs +++ b/src/server/gpodder/mod.rs @@ -1,18 +1,34 @@ -mod auth; -mod devices; +mod advanced; +mod models; +mod simple; use axum::{ - http::{HeaderName, HeaderValue}, - Router, + extract::{Request, State}, + http::{HeaderName, HeaderValue, StatusCode}, + middleware::Next, + response::{IntoResponse, Response}, + RequestExt, Router, +}; +use axum_extra::{ + extract::{ + cookie::{Cookie, Expiration}, + CookieJar, + }, + headers::{authorization::Basic, Authorization}, + TypedHeader, }; use tower_http::set_header::SetResponseHeaderLayer; +use crate::{db, server::error::AppError}; + use super::Context; +const SESSION_ID_COOKIE: &str = "sessionid"; + pub fn router(ctx: Context) -> Router { Router::new() - .nest("/auth", auth::router()) - .nest("/devices", devices::router(ctx)) + .merge(simple::router(ctx.clone())) + .nest("/api/2", advanced::router(ctx)) // https://gpoddernet.readthedocs.io/en/latest/api/reference/general.html#cors // All endpoints should send this CORS header value so the endpoints can be used from web // applications @@ -21,3 +37,75 @@ pub fn router(ctx: Context) -> Router { HeaderValue::from_static("*"), )) } + +/// This middleware accepts +pub async fn auth_middleware(State(ctx): State, 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; + + if let Some(session_id) = jar + .get(SESSION_ID_COOKIE) + .and_then(|c| c.value().parse::().ok()) + { + match tokio::task::spawn_blocking(move || db::Session::user_from_id(&ctx.pool, session_id)) + .await + .unwrap() + .map_err(AppError::Db) + { + Ok(user) => { + auth_user = user; + } + Err(err) => { + return err.into_response(); + } + }; + } else if let Ok(auth) = req + .extract_parts::>>() + .await + { + match tokio::task::spawn_blocking(move || { + let user = + db::User::by_username(&ctx.pool, auth.username())?.ok_or(AppError::NotFound)?; + + if user.verify_password(auth.password()) { + Ok((db::Session::new_for_user(&ctx.pool, user.id)?, user)) + } else { + Err(AppError::Unauthorized) + } + }) + .await + .unwrap() + { + Ok((session, user)) => { + auth_user = Some(user); + new_session_id = Some(session.id); + } + Err(err) => { + return err.into_response(); + } + } + } + + if let Some(user) = auth_user { + req.extensions_mut().insert(user); + let res = next.run(req).await; + + if let Some(session_id) = new_session_id { + ( + jar.add( + Cookie::build((SESSION_ID_COOKIE, session_id.to_string())) + .expires(Expiration::Session), + ), + res, + ) + .into_response() + } else { + res + } + } else { + StatusCode::UNAUTHORIZED.into_response() + } +} diff --git a/src/server/gpodder/models.rs b/src/server/gpodder/models.rs new file mode 100644 index 0000000..4e582dc --- /dev/null +++ b/src/server/gpodder/models.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize, Serialize}; + +use crate::db; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum DeviceType { + Desktop, + Laptop, + Mobile, + Server, + Other, +} + +impl From for db::DeviceType { + fn from(value: DeviceType) -> Self { + match value { + DeviceType::Desktop => Self::Desktop, + DeviceType::Laptop => Self::Laptop, + DeviceType::Mobile => Self::Mobile, + DeviceType::Server => Self::Server, + DeviceType::Other => Self::Other, + } + } +} + +impl From for 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, + } + } +} + +#[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/simple/mod.rs b/src/server/gpodder/simple/mod.rs new file mode 100644 index 0000000..299d671 --- /dev/null +++ b/src/server/gpodder/simple/mod.rs @@ -0,0 +1,7 @@ +use axum::Router; + +use crate::server::Context; + +pub fn router(ctx: Context) -> Router { + Router::new() +} diff --git a/src/server/mod.rs b/src/server/mod.rs index a6f5250..a1cbabb 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -11,7 +11,7 @@ pub struct Context { pub fn app(ctx: Context) -> Router { Router::new() - .nest("/api/2", gpodder::router(ctx.clone())) + .merge(gpodder::router(ctx.clone())) .layer(TraceLayer::new_for_http()) .with_state(ctx) }