feat: add separate auth middleware for web routes

main
Jef Roosens 2025-03-30 09:50:55 +02:00
parent b3e49be299
commit 3071685950
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
11 changed files with 89 additions and 19 deletions

View File

@ -8,7 +8,7 @@ use axum::{
use crate::server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
auth_api_middleware,
format::{Format, StringWithFormat},
models,
},
@ -19,7 +19,7 @@ pub fn router(ctx: Context) -> Router<Context> {
Router::new()
.route("/{username}", get(get_devices))
.route("/{username}/{id}", post(post_device))
.layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
.layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware))
}
async fn get_devices(

View File

@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use crate::server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
auth_api_middleware,
format::{Format, StringWithFormat},
models,
models::UpdatedUrlsResponse,
@ -24,7 +24,7 @@ pub fn router(ctx: Context) -> Router<Context> {
"/{username}",
post(post_episode_actions).get(get_episode_actions),
)
.layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
.layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware))
}
async fn post_episode_actions(

View File

@ -9,7 +9,7 @@ use serde::Deserialize;
use crate::server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
auth_api_middleware,
format::{Format, StringWithFormat},
models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
},
@ -22,7 +22,7 @@ pub fn router(ctx: Context) -> Router<Context> {
"/{username}/{id}",
post(post_subscription_changes).get(get_subscription_changes),
)
.layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
.layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware))
}
pub async fn post_subscription_changes(

View File

@ -8,7 +8,7 @@ use axum::{
use crate::server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
auth_api_middleware,
format::{Format, StringWithFormat},
models::{SyncStatus, SyncStatusDelta},
},
@ -21,7 +21,7 @@ pub fn router(ctx: Context) -> Router<Context> {
"/{username}",
get(get_sync_status).post(post_sync_status_changes),
)
.layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
.layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware))
}
pub async fn get_sync_status(

View File

@ -36,8 +36,13 @@ pub fn router(ctx: Context) -> Router<Context> {
))
}
/// This middleware accepts
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
/// Middleware that can authenticate both with session cookies and basic auth. If basic auth is
/// used, no session is created. If authentication fails, the server returns a 401.
pub async fn auth_api_middleware(
State(ctx): State<Context>,
mut req: Request,
next: Next,
) -> Response {
// SAFETY: this extractor's error type is Infallible
let mut jar: CookieJar = req.extract_parts().await.unwrap();
let mut auth_user = None;

View File

@ -7,7 +7,7 @@ use axum::{
use crate::server::{
error::{AppError, AppResult},
gpodder::{auth_middleware, format::StringWithFormat},
gpodder::{auth_api_middleware, format::StringWithFormat},
Context,
};
@ -18,7 +18,7 @@ pub fn router(ctx: Context) -> Router<Context> {
get(get_device_subscriptions).put(put_device_subscriptions),
)
.route("/{username}", get(get_user_subscriptions))
.layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
.layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware))
}
pub async fn get_device_subscriptions(

View File

@ -10,7 +10,8 @@ use axum::{
extract::Request,
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
response::{IntoResponse, Redirect, Response},
routing::get,
Router,
};
use http_body_util::BodyExt;
@ -27,6 +28,7 @@ pub fn app(ctx: Context) -> Router {
.merge(gpodder::router(ctx.clone()))
.nest("/static", r#static::router())
.nest("/_", web::router(ctx.clone()))
.route("/", get(|| async { Redirect::to("/_") }))
.layer(axum::middleware::from_fn(header_logger))
.layer(axum::middleware::from_fn(body_logger))
.layer(TraceLayer::new_for_http())

View File

@ -1,13 +1,73 @@
use axum::{extract::State, http::HeaderMap, routing::get, Router};
use axum::{
extract::{Request, State},
http::HeaderMap,
middleware::{self, Next},
response::{IntoResponse, Redirect, Response},
routing::get,
RequestExt, Router,
};
use axum_extra::extract::CookieJar;
use cookie::Cookie;
use crate::web::{Page, TemplateExt, TemplateResponse, View};
use super::Context;
use super::{error::AppError, Context};
pub fn router(_ctx: Context) -> Router<Context> {
Router::new().route("/", get(get_index))
const SESSION_ID_COOKIE: &str = "sessionid";
pub fn router(ctx: Context) -> Router<Context> {
Router::new()
.route("/", get(get_index))
.layer(middleware::from_fn_with_state(
ctx.clone(),
auth_web_middleware,
))
}
async fn get_index(State(ctx): State<Context>, headers: HeaderMap) -> TemplateResponse<Page<View>> {
View::Index.page(&headers).response(&ctx.tera)
}
/// Middleware that authenticates the current user via the session token. If the credentials are
/// invalid, the user is redirected to the login page.
pub async fn auth_web_middleware(
State(ctx): State<Context>,
mut req: Request,
next: Next,
) -> Response {
// SAFETY: this extractor's error type is Infallible
let mut jar: CookieJar = req.extract_parts().await.unwrap();
let redirect = Redirect::to("/_/login");
if let Some(session_id) = jar
.get(SESSION_ID_COOKIE)
.and_then(|c| c.value().parse::<i64>().ok())
{
match tokio::task::spawn_blocking(move || {
let session = ctx.store.get_session(session_id)?;
ctx.store.refresh_session(&session)?;
Ok(session)
})
.await
.unwrap()
{
Ok(session) => {
req.extensions_mut().insert(session.user);
(jar, next.run(req).await).into_response()
}
Err(gpodder::AuthErr::UnknownSession) => {
jar = jar.add(
Cookie::build((SESSION_ID_COOKIE, String::new()))
.max_age(cookie::time::Duration::ZERO),
);
(jar, redirect).into_response()
}
Err(err) => AppError::from(err).into_response(),
}
} else {
redirect.into_response()
}
}

View File

@ -71,7 +71,10 @@ pub fn initialize_tera() -> tera::Result<tera::Tera> {
tera.add_raw_templates([
(BASE_TEMPLATE, include_str!("templates/base.html")),
(View::Index.template(), include_str!("templates/index.html")),
(
View::Index.template(),
include_str!("templates/views/index.html"),
),
])?;
Ok(tera)

View File

@ -7,7 +7,7 @@ pub enum View {
impl Template for View {
fn template(&self) -> &'static str {
match self {
Self::Index => "index.html",
Self::Index => "views/index.html",
}
}