refactor: restructure using simple and advanced api

episode-actions
Jef Roosens 2025-02-24 10:42:59 +01:00
parent 4d37ddb780
commit 73928e78f4
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
8 changed files with 173 additions and 137 deletions

View File

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

View File

@ -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<Context> {
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<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;
if let Some(session_id) = jar
.get(SESSION_ID_COOKIE)
.and_then(|c| c.value().parse::<i64>().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::<TypedHeader<Authorization<Basic>>>()
.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()
}
}

View File

@ -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<Context> {
Router::new()
.route("/{username}", get(get_devices))
@ -23,48 +24,6 @@ pub fn router(ctx: Context) -> Router<Context> {
.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<DeviceType> 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<db::DeviceType> 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<Context>,
Path(username): Path<String>,
@ -94,12 +53,6 @@ async fn get_devices(
Ok(Json(devices))
}
#[derive(Deserialize)]
pub struct DevicePatch {
caption: Option<String>,
r#type: Option<DeviceType>,
}
async fn post_device(
State(ctx): State<Context>,
Path((_username, id)): Path<(String, String)>,

View File

@ -0,0 +1,12 @@
use axum::Router;
use crate::server::Context;
mod auth;
mod devices;
pub fn router(ctx: Context) -> Router<Context> {
Router::new()
.nest("/auth", auth::router())
.nest("/devices", devices::router(ctx.clone()))
}

View File

@ -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<Context> {
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<Context> {
HeaderValue::from_static("*"),
))
}
/// This middleware accepts
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;
if let Some(session_id) = jar
.get(SESSION_ID_COOKIE)
.and_then(|c| c.value().parse::<i64>().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::<TypedHeader<Authorization<Basic>>>()
.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()
}
}

View File

@ -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<DeviceType> 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<db::DeviceType> 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<String>,
pub r#type: Option<DeviceType>,
}

View File

@ -0,0 +1,7 @@
use axum::Router;
use crate::server::Context;
pub fn router(ctx: Context) -> Router<Context> {
Router::new()
}

View File

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