use axum::extract::{Query, State};
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use serde::Deserialize;
use crate::server::AppState;
const PORTAL_HTML: &str = include_str!("index.html");
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct PortalQuery {
pub origin: String,
}
pub async fn portal_handler(
State(state): State<AppState>,
Query(query): Query<PortalQuery>,
) -> Response {
let (allowed, webauthn_enabled) = {
let config = state.config.read().await;
(config.server.cors_origins.clone(), config.services.webauthn)
};
if !webauthn_enabled {
return (
StatusCode::SERVICE_UNAVAILABLE,
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
"<!doctype html><meta charset=\"utf-8\"><title>VTA Auth Portal</title>\
<body style=\"font-family:sans-serif;padding:2rem;color:#d65a5a;\">\
<h1>503 — WebAuthn service disabled</h1>\
<p>This VTA does not currently advertise a WebAuthn-RP surface. \
The operator can re-enable with <code>pnm services webauthn enable --url <url></code>.</p>\
</body>",
)
.into_response();
}
if !allowed.iter().any(|o| o == &query.origin) {
return (
StatusCode::FORBIDDEN,
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
format!(
"<!doctype html><meta charset=\"utf-8\"><title>VTA Auth Portal</title>\
<body style=\"font-family:sans-serif;padding:2rem;color:#d65a5a;\">\
<h1>403 — origin not allowed</h1>\
<p>The origin <code>{}</code> is not in this VTA's <code>server.cors_origins</code> \
allowlist. Ask the VTA operator to add it before retrying.</p>\
</body>",
html_escape(&query.origin),
),
)
.into_response();
}
(
StatusCode::OK,
[
(header::CONTENT_TYPE, "text/html; charset=utf-8"),
(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate"),
],
PORTAL_HTML,
)
.into_response()
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[allow(dead_code)]
fn _hv_alive(_: HeaderValue) {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn html_escape_handles_common_xss_chars() {
assert_eq!(
html_escape("<script>alert('x')</script>"),
"<script>alert('x')</script>",
);
assert_eq!(html_escape("a & b"), "a & b");
assert_eq!(html_escape("\"quoted\""), ""quoted"");
}
#[test]
fn portal_html_contains_expected_anchors() {
assert!(PORTAL_HTML.contains("vta-portal-result"));
assert!(PORTAL_HTML.contains("vta-portal-ready"));
assert!(PORTAL_HTML.contains("vta-portal-config"));
assert!(PORTAL_HTML.contains("postMessage"));
assert!(PORTAL_HTML.contains("/auth/passkey-login/start"));
assert!(PORTAL_HTML.contains("/auth/passkey-login/finish"));
assert!(PORTAL_HTML.contains("/did/verification-methods/passkey"));
}
}