refactor: adapters configured from config crate

This commit is contained in:
2026-10-25 21:07:19 +01:00
parent 1f724bb064
commit 04f9c13625
45 changed files with 277 additions and 177 deletions

View File

@@ -1,5 +1,5 @@
use axum::{
extract::Request,
extract::{Request, State},
http::{HeaderValue, header},
middleware::Next,
response::Response,
@@ -19,15 +19,19 @@ pub fn extract_from_cookie(headers: &axum::http::HeaderMap) -> Option<String> {
})
}
pub(crate) fn secure_flag() -> &'static str {
if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") {
"; Secure"
} else {
""
}
/// Router state for [`csrf_middleware`]: whether cookies carry the `Secure` attribute.
#[derive(Clone, Copy)]
pub struct SecureCookies(pub bool);
pub(crate) fn secure_flag(secure_cookies: bool) -> &'static str {
if secure_cookies { "; Secure" } else { "" }
}
pub async fn csrf_middleware(mut req: Request, next: Next) -> Response {
pub async fn csrf_middleware(
State(SecureCookies(secure_cookies)): State<SecureCookies>,
mut req: Request,
next: Next,
) -> Response {
let existing = extract_from_cookie(req.headers());
let (token, needs_set) = match existing {
Some(t) => (t, false),
@@ -42,7 +46,7 @@ pub async fn csrf_middleware(mut req: Request, next: Next) -> Response {
let cookie = format!(
"csrf={}; HttpOnly; Path=/; SameSite=Strict{}",
token,
secure_flag()
secure_flag(secure_cookies)
);
if let Ok(val) = HeaderValue::from_str(&cookie) {
response.headers_mut().append(header::SET_COOKIE, val);

View File

@@ -25,12 +25,16 @@ use template_askama::{LoginTemplate, RegisterTemplate};
// ── HTML helpers ─────────────────────────────────────────────────────────────
fn set_cookie_header(token: &str, max_age: i64) -> (axum::http::HeaderName, HeaderValue) {
fn set_cookie_header(
token: &str,
max_age: i64,
secure_cookies: bool,
) -> (axum::http::HeaderName, HeaderValue) {
let val = format!(
"token={}; HttpOnly; Path=/; SameSite=Strict; Max-Age={}{}",
token,
max_age,
secure_flag()
secure_flag(secure_cookies)
);
(
SET_COOKIE,
@@ -177,17 +181,18 @@ pub async fn post_login(
{
Ok(result) => {
let max_age = (result.expires_at - Utc::now()).num_seconds().max(0);
let cookie = set_cookie_header(&result.token, max_age);
let cookie =
set_cookie_header(&result.token, max_age, state.app_ctx.config.secure_cookies);
([cookie], Redirect::to("/")).into_response()
}
Err(_) => Redirect::to("/login?error=Invalid+credentials").into_response(),
}
}
pub async fn get_logout() -> impl IntoResponse {
pub async fn get_logout(State(state): State<AppState>) -> impl IntoResponse {
let val = format!(
"token=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0{}",
secure_flag()
secure_flag(state.app_ctx.config.secure_cookies)
);
let cookie = (
SET_COOKIE,
@@ -246,7 +251,8 @@ pub async fn post_register(
{
Ok(result) => {
let max_age = (result.expires_at - Utc::now()).num_seconds().max(0);
let cookie = set_cookie_header(&result.token, max_age);
let cookie =
set_cookie_header(&result.token, max_age, state.app_ctx.config.secure_cookies);
([cookie], Redirect::to("/")).into_response()
}
Err(_) => {

View File

@@ -12,6 +12,8 @@ use crate::{handlers, state::AppState};
pub fn build_router(state: AppState, ap_router: Router) -> Router {
let rate_limit = state.app_ctx.config.rate_limit;
let secure_cookies = state.app_ctx.config.secure_cookies;
let cors_origins = state.app_ctx.config.cors_origins.clone();
let ap_cfg = GovernorConfigBuilder::default()
.with_extractor(PeerIp::default())
@@ -23,8 +25,8 @@ pub fn build_router(state: AppState, ap_router: Router) -> Router {
Router::new()
.route("/health", routing::get(health_handler))
.merge(html_routes(rate_limit))
.merge(api_routes(rate_limit))
.merge(html_routes(rate_limit, secure_cookies))
.merge(api_routes(rate_limit, &cors_origins))
.nest_service("/static", ServeDir::new("static"))
.nest_service(
"/app",
@@ -44,7 +46,7 @@ fn per_minute(n: u64) -> Quota {
Quota::requests_per_minute(n)
}
fn html_routes(rate_limit: u64) -> Router<AppState> {
fn html_routes(rate_limit: u64, secure_cookies: bool) -> Router<AppState> {
let auth = Router::new()
.route(
"/login",
@@ -188,7 +190,10 @@ fn html_routes(rate_limit: u64) -> Router<AppState> {
#[cfg(feature = "federation")]
let base = base.merge(federation_html_routes());
base.layer(axum::middleware::from_fn(crate::csrf::csrf_middleware))
base.layer(axum::middleware::from_fn_with_state(
crate::csrf::SecureCookies(secure_cookies),
crate::csrf::csrf_middleware,
))
}
fn social_html_routes() -> Router<AppState> {
@@ -278,11 +283,10 @@ fn federation_html_routes() -> Router<AppState> {
)
}
fn cors_layer() -> CorsLayer {
fn cors_layer(origins: &str) -> CorsLayer {
use axum::http::{HeaderName, Method};
use tower_http::cors::AllowOrigin;
let origins = std::env::var("CORS_ORIGINS").unwrap_or_default();
let layer = CorsLayer::new()
.allow_methods([
Method::GET,
@@ -319,7 +323,7 @@ fn cors_layer() -> CorsLayer {
}
}
fn api_routes(rate_limit: u64) -> Router<AppState> {
fn api_routes(rate_limit: u64, cors_origins: &str) -> Router<AppState> {
let cfg = GovernorConfigBuilder::default()
.with_extractor(PeerIp::default())
.expect_connect_info()
@@ -506,7 +510,7 @@ fn api_routes(rate_limit: u64) -> Router<AppState> {
Router::new()
.nest("/api/v1", base.layer(GovernorLayer::new(cfg)))
.nest("/api/v1", webhook_routes)
.layer(cors_layer())
.layer(cors_layer(cors_origins))
}
fn social_api_routes() -> Router<AppState> {

View File

@@ -843,6 +843,8 @@ pub(crate) fn build_test_state_from(
base_url: "http://localhost:3000".to_string(),
rate_limit: 20,
refresh_ttl_seconds: 2_592_000,
secure_cookies: false,
cors_origins: String::new(),
wrapup: application::config::WrapUpConfig {
font_path: None,
logo_path: None,