feat: get rid of all javascript
implements a generic solution to redirect query strings generated by forms to path based URIs that can be handled by existing routes. This eliminates the need for javascript that was doing the equivalent client-side.
This commit is contained in:
parent
22f06180ae
commit
29834dbe50
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -1485,6 +1485,16 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-normalization",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
@ -1823,6 +1833,7 @@ dependencies = [
|
|||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3377,6 +3388,12 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-bidi"
|
||||||
|
version = "0.3.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
@ -3420,6 +3437,17 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"idna",
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@ -26,6 +26,7 @@ boilerplate = { version = "1.0.0" }
|
|||||||
html-escaper = "0.2.0"
|
html-escaper = "0.2.0"
|
||||||
tower-http = { version = "0.5.2", features = ["fs"] }
|
tower-http = { version = "0.5.2", features = ["fs"] }
|
||||||
readonly = "0.2.12"
|
readonly = "0.2.12"
|
||||||
|
url = "2.5.0"
|
||||||
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
pub mod block;
|
pub mod block;
|
||||||
pub mod not_found;
|
pub mod not_found;
|
||||||
|
pub mod redirect_qs_to_path;
|
||||||
pub mod root;
|
pub mod root;
|
||||||
pub mod utxo;
|
pub mod utxo;
|
||||||
|
|||||||
116
src/html/page/redirect_qs_to_path.rs
Normal file
116
src/html/page/redirect_qs_to_path.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use axum::extract::RawQuery;
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum::response::Redirect;
|
||||||
|
use axum::response::Response;
|
||||||
|
use std::sync::Arc;
|
||||||
|
// use axum::routing::get;
|
||||||
|
// use axum::routing::Router;
|
||||||
|
use super::not_found::not_found_html_response;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
// use super::root::root;
|
||||||
|
// use super::utxo::utxo_page;
|
||||||
|
use crate::model::app_state::AppState;
|
||||||
|
// use neptune_explorer::model::config::Config;
|
||||||
|
|
||||||
|
/// This converts a query string into a path and redirects browser.
|
||||||
|
///
|
||||||
|
/// Purpose: enable a javascript-free website.
|
||||||
|
///
|
||||||
|
/// Problem being solved:
|
||||||
|
///
|
||||||
|
/// Our axum routes are all paths, however an HTML form submits user input as a query-string.
|
||||||
|
/// We need to convert that query string into a path.
|
||||||
|
///
|
||||||
|
/// We also want the end-user to see a nice path in the browser url
|
||||||
|
/// for purposes of copy/paste, etc.
|
||||||
|
///
|
||||||
|
/// Browser form submits: We want:
|
||||||
|
/// /utxo?utxo=5&l=Submit /utxo/5
|
||||||
|
/// /block?height=15&l=Submit /block/height/15
|
||||||
|
///
|
||||||
|
/// Solution:
|
||||||
|
///
|
||||||
|
/// 1. We submit all browser forms to /rqs with method=get.
|
||||||
|
/// (note: rqs is short for redirect-query-string)
|
||||||
|
/// 2. /rqs calls this redirect_query_string_to_path() handler.
|
||||||
|
/// 3. query-string is transformed to path as follows:
|
||||||
|
/// a) if _ig key is present, the value is split by ','
|
||||||
|
/// to obtain a list of query-string keys to ignore.
|
||||||
|
/// b. each key/val is converted to:
|
||||||
|
/// key (if val is empty)
|
||||||
|
/// key/val (if val is not empty)
|
||||||
|
/// c) any keys in the _ig list are ignored.
|
||||||
|
/// d) keys and vals are url encoded
|
||||||
|
/// e) each resulting /key or /key/val is appended to the path.
|
||||||
|
/// 4. a 301 redirect to the new path is sent to the browser.
|
||||||
|
///
|
||||||
|
/// An html form might look like:
|
||||||
|
///
|
||||||
|
/// <form action="/rqs" method="get">
|
||||||
|
/// <input type="hidden" name="block" value=""/>
|
||||||
|
/// <input type="hidden" name="_ig" value="l"/>
|
||||||
|
///
|
||||||
|
/// Block height or digest:
|
||||||
|
/// <input type="text" size="80" name="height_or_digest"/>
|
||||||
|
/// <input type="submit" name="l" value="Lookup Block"/>
|
||||||
|
/// </form>
|
||||||
|
///
|
||||||
|
/// note that the submit with name "l" is ignored because
|
||||||
|
/// of _ig=l. We could ignore a list of fields also
|
||||||
|
/// eg _ig=field1,field2,field3,etc.
|
||||||
|
///
|
||||||
|
/// Order of keys in the query-string (and form) is important.
|
||||||
|
///
|
||||||
|
/// Any keys that are not ignored are translated into a
|
||||||
|
/// path in the order received. Eg:
|
||||||
|
/// /rqs?block=&height_or_digest=10 --> /block/height_or_digest/10
|
||||||
|
/// /rqs?height_or_digest=10&block= --> /height_or_digest/10/block
|
||||||
|
///
|
||||||
|
/// A future enhancement could be to add an optional field for specifying the
|
||||||
|
/// path order. That would enable re-ordering of inputs in a form without
|
||||||
|
/// altering the resulting path. For now, our forms are so simple, that is not
|
||||||
|
/// needed.
|
||||||
|
#[axum::debug_handler]
|
||||||
|
pub async fn redirect_query_string_to_path(
|
||||||
|
RawQuery(raw_query_option): RawQuery,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> Result<Response, Response> {
|
||||||
|
let not_found = || not_found_html_response(State(state.clone()), None);
|
||||||
|
|
||||||
|
let raw_query = raw_query_option.ok_or_else(not_found)?;
|
||||||
|
|
||||||
|
// note: we construct a fake-url so we can use Url::query_pairs().
|
||||||
|
let fake_url = format!("http://127.0.0.1/?{}", raw_query);
|
||||||
|
let url = url::Url::parse(&fake_url).map_err(|_| not_found())?;
|
||||||
|
let query_vars: Vec<(String, _)> = url.query_pairs().into_owned().collect();
|
||||||
|
|
||||||
|
const IGNORE_QS_VAR: &str = "_ig";
|
||||||
|
|
||||||
|
let ignore_keys: HashSet<_> = match query_vars.iter().find(|(k, _)| k == IGNORE_QS_VAR) {
|
||||||
|
Some((_k, v)) => v.split(',').collect(),
|
||||||
|
None => Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut new_path: String = Default::default();
|
||||||
|
|
||||||
|
for (key, val) in query_vars.iter() {
|
||||||
|
if key == IGNORE_QS_VAR || ignore_keys.contains(key as &str) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parts = match val.is_empty() {
|
||||||
|
false => format!("/{}/{}", url_encode(key), url_encode(val)),
|
||||||
|
true => format!("/{}", url_encode(key)),
|
||||||
|
};
|
||||||
|
new_path += &parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
match new_path.is_empty() {
|
||||||
|
true => Err(not_found()),
|
||||||
|
false => Ok(Redirect::permanent(&new_path).into_response()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url_encode(s: &str) -> String {
|
||||||
|
url::form_urlencoded::byte_serialize(s.as_bytes()).collect()
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ use neptune_core::rpc_server::RPCClient;
|
|||||||
use neptune_explorer::html::page::block::block_page;
|
use neptune_explorer::html::page::block::block_page;
|
||||||
use neptune_explorer::html::page::block::block_page_with_value;
|
use neptune_explorer::html::page::block::block_page_with_value;
|
||||||
use neptune_explorer::html::page::not_found::not_found_html_fallback;
|
use neptune_explorer::html::page::not_found::not_found_html_fallback;
|
||||||
|
use neptune_explorer::html::page::redirect_qs_to_path::redirect_query_string_to_path;
|
||||||
use neptune_explorer::html::page::root::root;
|
use neptune_explorer::html::page::root::root;
|
||||||
use neptune_explorer::html::page::utxo::utxo_page;
|
use neptune_explorer::html::page::utxo::utxo_page;
|
||||||
use neptune_explorer::model::app_state::AppState;
|
use neptune_explorer::model::app_state::AppState;
|
||||||
@ -58,6 +59,8 @@ async fn main() -> Result<(), RpcError> {
|
|||||||
.route("/block/:selector", get(block_page))
|
.route("/block/:selector", get(block_page))
|
||||||
.route("/block/:selector/:value", get(block_page_with_value))
|
.route("/block/:selector/:value", get(block_page_with_value))
|
||||||
.route("/utxo/:value", get(utxo_page))
|
.route("/utxo/:value", get(utxo_page))
|
||||||
|
// -- Rewrite query-strings to path --
|
||||||
|
.route("/rqs", get(redirect_query_string_to_path))
|
||||||
// -- Static files --
|
// -- Static files --
|
||||||
.nest_service(
|
.nest_service(
|
||||||
"/css",
|
"/css",
|
||||||
|
|||||||
@ -2,22 +2,6 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>{{self.state.config.site_name}}: (network: {{self.state.network}})</title>
|
<title>{{self.state.config.site_name}}: (network: {{self.state.network}})</title>
|
||||||
{{ html_escaper::Trusted(include_str!( concat!(env!("CARGO_MANIFEST_DIR"), "/templates/web/html/components/head.html"))) }}
|
{{ html_escaper::Trusted(include_str!( concat!(env!("CARGO_MANIFEST_DIR"), "/templates/web/html/components/head.html"))) }}
|
||||||
<script>
|
|
||||||
function handle_submit(form) {
|
|
||||||
let value = form.height_or_digest.value;
|
|
||||||
var is_digest = value.length == 80;
|
|
||||||
var type = is_digest ? "digest" : "height";
|
|
||||||
var uri = form.action + "/" + type + "/" + value;
|
|
||||||
window.location.href = uri;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
function handle_utxo_submit(form) {
|
|
||||||
let value = form.utxo.value;
|
|
||||||
var uri = form.action + "/" + value;
|
|
||||||
window.location.href = uri;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="container">
|
<header class="container">
|
||||||
@ -35,7 +19,9 @@ The blockchain tip is at height: {{self.tip_height}}
|
|||||||
<summary>
|
<summary>
|
||||||
Block Lookup
|
Block Lookup
|
||||||
</summary>
|
</summary>
|
||||||
<form action="/block" method="get" onsubmit="return handle_submit(this)">
|
<form action="/rqs" method="get">
|
||||||
|
<input type="hidden" name="block" value="" />
|
||||||
|
<input type="hidden" name="_ig" value="l"/>
|
||||||
<span class="tooltip">🛈
|
<span class="tooltip">🛈
|
||||||
<span class="tooltiptext">
|
<span class="tooltiptext">
|
||||||
Provide a numeric block height or hexadecimal digest identifier to lookup any block in the Neptune blockchain.
|
Provide a numeric block height or hexadecimal digest identifier to lookup any block in the Neptune blockchain.
|
||||||
@ -44,7 +30,7 @@ The blockchain tip is at height: {{self.tip_height}}
|
|||||||
|
|
||||||
Block height or digest:
|
Block height or digest:
|
||||||
<input type="text" size="80" name="height_or_digest" class="mono"/>
|
<input type="text" size="80" name="height_or_digest" class="mono"/>
|
||||||
<input type="submit" name="height" value="Lookup Block"/>
|
<input type="submit" name="l" value="Lookup Block"/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
Quick Lookup:
|
Quick Lookup:
|
||||||
@ -56,7 +42,8 @@ Quick Lookup:
|
|||||||
<article>
|
<article>
|
||||||
<details open>
|
<details open>
|
||||||
<summary>UTXO Lookup</summary>
|
<summary>UTXO Lookup</summary>
|
||||||
<form action="/utxo" method="get" onsubmit="return handle_utxo_submit(this)">
|
<form action="/rqs" method="get">
|
||||||
|
<input type="hidden" name="_ig" value="l" />
|
||||||
<span class="tooltip">🛈
|
<span class="tooltip">🛈
|
||||||
<span class="tooltiptext">
|
<span class="tooltiptext">
|
||||||
An Unspent Transaction Output (UTXO) index can be found in the output of <i>neptune-cli wallet-status</i>. Look for the field: <b>aocl_leaf_index</b>
|
An Unspent Transaction Output (UTXO) index can be found in the output of <i>neptune-cli wallet-status</i>. Look for the field: <b>aocl_leaf_index</b>
|
||||||
@ -64,7 +51,7 @@ Quick Lookup:
|
|||||||
</span>
|
</span>
|
||||||
UTXO index:
|
UTXO index:
|
||||||
<input type="text" size="10" name="utxo" />
|
<input type="text" size="10" name="utxo" />
|
||||||
<input type="submit" name="height" value="Lookup Utxo" />
|
<input type="submit" name="l" value="Lookup Utxo" />
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user