tower-surf 0.3.0

🌊 A stateless CSRF middleware for tower.
Documentation
use std::net::SocketAddr;

use axum::{
    response::Redirect,
    routing::{get, post},
    Router,
};
use http::StatusCode;
use maud::{html, Markup};
use tower_surf::{Surf, Token};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(root))
        .route("/submit", post(submit))
        .route("/login", get(login))
        .route("/logout", get(logout))
        .layer(Surf::new("secret-key").secure(false));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();

    axum::serve(listener, app.into_make_service())
        .await
        .unwrap();
}

async fn root(token: Token) -> Result<Markup, StatusCode> {
    let token = token.get().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok(html! {
        link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css";
        script src="https://unpkg.com/htmx.org@2.0.2" {}

        main class="container" {
            nav {
                ul {
                    li { a href="/login" { "Login" } }
                    li { a href="/logout" { "Logout" } }
                }
            }

            p { mark { "Open the Network tab in your dev console." } }
            p { small { kbd { (token) } } }

            div class="grid" {
                div {
                    form hx-post="/submit" hx-swap="none" "hx-on::config-request"={"event.detail.headers['X-CSRF-Token'] = \"" (token) "\""} {
                        label for="hotdogs" { "How do you like your hotdogs?" }

                        select name="hotdogs" value="ketchup" {
                            option value="ketchup" { "Ketchup" }
                            option value="ketchup-again" { "Ketchup" }
                            option value="more-ketchup" { "More ketchup" }
                        }

                        button type="submit" { "Submit with token" }
                    }
                }

                div {
                    form hx-post="/submit" {
                        label for="hotdogs" { "How do you like your hotdogs?" }

                        select name="hotdogs" value="ketchup" {
                            option value="ketchup" { "Ketchup" }
                            option value="still-ketchup" { "Still ketchup" }
                            option value="always-ketchup" { "It'll always be ketchup!" }
                        }

                        button type="submit" { "Submit without token" }
                    }
                }
            }
        }
    })
}

async fn submit() -> (StatusCode, &'static str) {
    (StatusCode::OK, "Success!")
}

async fn login(token: Token) -> Result<Redirect, (StatusCode, String)> {
    token
        .set("secret-session-identifier")
        .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;

    Ok(Redirect::to("/"))
}

async fn logout(token: Token) -> Redirect {
    token.reset();

    Redirect::to("/")
}