//! The few HTML pages the server shows during sign-in. //! //! Hand-written and script-free: three small pages don't justify a template //! engine, and with no JavaScript the content security policy can forbid it //! outright. use axum::http::header::{CACHE_CONTROL, CONTENT_SECURITY_POLICY, X_FRAME_OPTIONS}; use axum::http::{HeaderValue, StatusCode}; use axum::response::{Html, IntoResponse, Response}; use crate::db::PendingLogin; /// The sign-in page: a username/password form, plus a button for the /// identity provider when one is configured. `username` refills the form after /// a failed attempt, so only the password needs retyping. pub fn login( pending: &PendingLogin, oidc_name: Option<&str>, error: Option<&str>, username: &str, ) -> Response { let error = error .map(|e| format!(r#"
{}
"#, esc(e))) .unwrap_or_default(); let oidc = oidc_name .map(|name| { format!( r#"Sign in with {name}Signing in {device} to sync your library.
{error} {oidc} "#, device = esc(&pending.device_name), id = esc(&pending.id), username = esc(username), focus_user = if username.is_empty() { " autofocus" } else { "" }, focus_password = if username.is_empty() { "" } else { " autofocus" }, ); page(StatusCode::OK, "Sign in", &body, "") } /// Sign-in finished: send the browser back to the app. /// /// A page rather than a bare redirect, because a phone browser may decline to /// follow a redirect into an app on its own; the button then gives the user a /// tap that it will honour. pub fn return_to_app(url: &str) -> Response { let url = esc(url); let body = format!( r#"If the app doesn't open by itself, use the button below. You can close this tab afterwards.
Return to mtgc"# ); // Meta refresh rather than a 30x, so the page above is what's left showing // if the hand-off doesn't happen. let refresh = format!(r#""#); page(StatusCode::OK, "Signed in", &body, &refresh) } pub fn error(status: StatusCode, message: &str) -> Response { let body = format!( r#"{}
"#, esc(message) ); page(status, "Sign-in problem", &body, "") } fn page(status: StatusCode, title: &str, body: &str, head_extra: &str) -> Response { let mut resp = (status, Html(render(title, body, head_extra))).into_response(); let h = resp.headers_mut(); h.insert( CONTENT_SECURITY_POLICY, HeaderValue::from_static("default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'"), ); // The password page must never be framed by another site. h.insert(X_FRAME_OPTIONS, HeaderValue::from_static("DENY")); // Each page embeds a single-use id or code. h.insert(CACHE_CONTROL, HeaderValue::from_static("no-store")); resp } fn render(title: &str, body: &str, head_extra: &str) -> String { format!( r#" {head_extra}