feat: add separate auth middleware for web routes
parent
b3e49be299
commit
3071685950
|
@ -8,7 +8,7 @@ use axum::{
|
||||||
use crate::server::{
|
use crate::server::{
|
||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
gpodder::{
|
gpodder::{
|
||||||
auth_middleware,
|
auth_api_middleware,
|
||||||
format::{Format, StringWithFormat},
|
format::{Format, StringWithFormat},
|
||||||
models,
|
models,
|
||||||
},
|
},
|
||||||
|
@ -19,7 +19,7 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/{username}", get(get_devices))
|
.route("/{username}", get(get_devices))
|
||||||
.route("/{username}/{id}", post(post_device))
|
.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(
|
async fn get_devices(
|
||||||
|
|
|
@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::server::{
|
use crate::server::{
|
||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
gpodder::{
|
gpodder::{
|
||||||
auth_middleware,
|
auth_api_middleware,
|
||||||
format::{Format, StringWithFormat},
|
format::{Format, StringWithFormat},
|
||||||
models,
|
models,
|
||||||
models::UpdatedUrlsResponse,
|
models::UpdatedUrlsResponse,
|
||||||
|
@ -24,7 +24,7 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
"/{username}",
|
"/{username}",
|
||||||
post(post_episode_actions).get(get_episode_actions),
|
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(
|
async fn post_episode_actions(
|
||||||
|
|
|
@ -9,7 +9,7 @@ use serde::Deserialize;
|
||||||
use crate::server::{
|
use crate::server::{
|
||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
gpodder::{
|
gpodder::{
|
||||||
auth_middleware,
|
auth_api_middleware,
|
||||||
format::{Format, StringWithFormat},
|
format::{Format, StringWithFormat},
|
||||||
models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
|
models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
|
||||||
},
|
},
|
||||||
|
@ -22,7 +22,7 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
"/{username}/{id}",
|
"/{username}/{id}",
|
||||||
post(post_subscription_changes).get(get_subscription_changes),
|
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(
|
pub async fn post_subscription_changes(
|
||||||
|
|
|
@ -8,7 +8,7 @@ use axum::{
|
||||||
use crate::server::{
|
use crate::server::{
|
||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
gpodder::{
|
gpodder::{
|
||||||
auth_middleware,
|
auth_api_middleware,
|
||||||
format::{Format, StringWithFormat},
|
format::{Format, StringWithFormat},
|
||||||
models::{SyncStatus, SyncStatusDelta},
|
models::{SyncStatus, SyncStatusDelta},
|
||||||
},
|
},
|
||||||
|
@ -21,7 +21,7 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
"/{username}",
|
"/{username}",
|
||||||
get(get_sync_status).post(post_sync_status_changes),
|
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(
|
pub async fn get_sync_status(
|
||||||
|
|
|
@ -36,8 +36,13 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This middleware accepts
|
/// Middleware that can authenticate both with session cookies and basic auth. If basic auth is
|
||||||
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
|
/// 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
|
// SAFETY: this extractor's error type is Infallible
|
||||||
let mut jar: CookieJar = req.extract_parts().await.unwrap();
|
let mut jar: CookieJar = req.extract_parts().await.unwrap();
|
||||||
let mut auth_user = None;
|
let mut auth_user = None;
|
||||||
|
|
|
@ -7,7 +7,7 @@ use axum::{
|
||||||
|
|
||||||
use crate::server::{
|
use crate::server::{
|
||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
gpodder::{auth_middleware, format::StringWithFormat},
|
gpodder::{auth_api_middleware, format::StringWithFormat},
|
||||||
Context,
|
Context,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
get(get_device_subscriptions).put(put_device_subscriptions),
|
get(get_device_subscriptions).put(put_device_subscriptions),
|
||||||
)
|
)
|
||||||
.route("/{username}", get(get_user_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(
|
pub async fn get_device_subscriptions(
|
||||||
|
|
|
@ -10,7 +10,8 @@ use axum::{
|
||||||
extract::Request,
|
extract::Request,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
|
@ -27,6 +28,7 @@ pub fn app(ctx: Context) -> Router {
|
||||||
.merge(gpodder::router(ctx.clone()))
|
.merge(gpodder::router(ctx.clone()))
|
||||||
.nest("/static", r#static::router())
|
.nest("/static", r#static::router())
|
||||||
.nest("/_", web::router(ctx.clone()))
|
.nest("/_", web::router(ctx.clone()))
|
||||||
|
.route("/", get(|| async { Redirect::to("/_") }))
|
||||||
.layer(axum::middleware::from_fn(header_logger))
|
.layer(axum::middleware::from_fn(header_logger))
|
||||||
.layer(axum::middleware::from_fn(body_logger))
|
.layer(axum::middleware::from_fn(body_logger))
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
|
|
|
@ -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 crate::web::{Page, TemplateExt, TemplateResponse, View};
|
||||||
|
|
||||||
use super::Context;
|
use super::{error::AppError, Context};
|
||||||
|
|
||||||
pub fn router(_ctx: Context) -> Router<Context> {
|
const SESSION_ID_COOKIE: &str = "sessionid";
|
||||||
Router::new().route("/", get(get_index))
|
|
||||||
|
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>> {
|
async fn get_index(State(ctx): State<Context>, headers: HeaderMap) -> TemplateResponse<Page<View>> {
|
||||||
View::Index.page(&headers).response(&ctx.tera)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -71,7 +71,10 @@ pub fn initialize_tera() -> tera::Result<tera::Tera> {
|
||||||
|
|
||||||
tera.add_raw_templates([
|
tera.add_raw_templates([
|
||||||
(BASE_TEMPLATE, include_str!("templates/base.html")),
|
(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)
|
Ok(tera)
|
||||||
|
|
|
@ -7,7 +7,7 @@ pub enum View {
|
||||||
impl Template for View {
|
impl Template for View {
|
||||||
fn template(&self) -> &'static str {
|
fn template(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Index => "index.html",
|
Self::Index => "views/index.html",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue