use axum::{ body::Body, extract::{Request, State}, http::{HeaderMap, StatusCode, header}, middleware::Next, response::{Html, IntoResponse, Redirect, Response}, Form, }; use sha2::{Digest, Sha256}; use serde::Deserialize; use super::WebState; /// Cookie name used to store the session token. const SESSION_COOKIE: &str = "furumi_session"; /// Compute SHA-256 of the token as hex string (stored in cookie, not raw token). pub fn token_hash(token: &str) -> String { let mut h = Sha256::new(); h.update(token.as_bytes()); format!("{:x}", h.finalize()) } /// axum middleware: if token is configured, requires a valid session cookie. pub async fn require_auth( State(state): State, req: Request, next: Next, ) -> Response { // Auth disabled when token is empty if state.token.is_empty() { return next.run(req).await; } let cookies = req .headers() .get(header::COOKIE) .and_then(|v| v.to_str().ok()) .unwrap_or(""); let expected = token_hash(&state.token); let authed = cookies .split(';') .any(|c| c.trim() == format!("{}={}", SESSION_COOKIE, expected)); if authed { next.run(req).await } else { let uri = req.uri().path(); if uri.starts_with("/api/") { (StatusCode::UNAUTHORIZED, "Unauthorized").into_response() } else { Redirect::to("/login").into_response() } } } /// GET /login — show login form. pub async fn login_page(State(state): State) -> impl IntoResponse { if state.token.is_empty() { return Redirect::to("/").into_response(); } Html(LOGIN_HTML).into_response() } #[derive(Deserialize)] pub struct LoginForm { password: String, } /// POST /login — validate password, set session cookie. pub async fn login_submit( State(state): State, Form(form): Form, ) -> impl IntoResponse { if state.token.is_empty() { return Redirect::to("/").into_response(); } if form.password == *state.token { let hash = token_hash(&state.token); let cookie = format!( "{}={}; HttpOnly; SameSite=Strict; Path=/", SESSION_COOKIE, hash ); let mut headers = HeaderMap::new(); headers.insert(header::SET_COOKIE, cookie.parse().unwrap()); headers.insert(header::LOCATION, "/".parse().unwrap()); (StatusCode::FOUND, headers, Body::empty()).into_response() } else { Html(LOGIN_ERROR_HTML).into_response() } } /// GET /logout — clear session cookie and redirect to login. pub async fn logout() -> impl IntoResponse { let cookie = format!( "{}=; HttpOnly; SameSite=Strict; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT", SESSION_COOKIE ); let mut headers = HeaderMap::new(); headers.insert(header::SET_COOKIE, cookie.parse().unwrap()); headers.insert(header::LOCATION, "/login".parse().unwrap()); (StatusCode::FOUND, headers, Body::empty()).into_response() } const LOGIN_HTML: &str = r#" Furumi Player — Login
Enter access token to continue
"#; const LOGIN_ERROR_HTML: &str = r#" Furumi Player — Login
Enter access token to continue

❌ Invalid token. Please try again.

"#;