Verify recovery support before spending and route node catalogs through nginx
This commit is contained in:
@@ -83,7 +83,7 @@ const RUNTIME_ASSETS_DIR: &str = "/opt/archipelago/web-ui/archipelago-runtime";
|
|||||||
/// Inserted into every server block of the nginx config that lacks the
|
/// Inserted into every server block of the nginx config that lacks the
|
||||||
/// `/api/app-catalog` proxy. Kept in sync with the canonical block in
|
/// `/api/app-catalog` proxy. Kept in sync with the canonical block in
|
||||||
/// image-recipe/configs/nginx-archipelago.conf.
|
/// image-recipe/configs/nginx-archipelago.conf.
|
||||||
const NGINX_APP_CATALOG_BLOCK: &str = "\n # App Store catalog proxy — backend fetches from configured registries\n # so the browser doesn't hit CORS/CSP. Without this block nginx falls\n # through to the SPA index.html and the frontend gets HTML back instead\n # of JSON.\n location /api/app-catalog {\n proxy_pass http://127.0.0.1:5678;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header Cookie $http_cookie;\n proxy_connect_timeout 15s;\n proxy_read_timeout 30s;\n proxy_send_timeout 15s;\n error_page 502 503 = @backend_unavailable;\n error_page 504 = @backend_timeout;\n }\n\n";
|
const NGINX_APP_CATALOG_BLOCK: &str = "\n # App Store catalog proxy — backend fetches from configured registries\n # so the browser doesn't hit CORS/CSP. Without this block nginx falls\n # through to the SPA index.html and the frontend gets HTML back instead\n # of JSON.\n location ~ ^/api/(?:app-catalog|node-app-catalog)$ {\n proxy_pass http://127.0.0.1:5678;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header Cookie $http_cookie;\n proxy_connect_timeout 15s;\n proxy_read_timeout 30s;\n proxy_send_timeout 15s;\n error_page 502 503 = @backend_unavailable;\n error_page 504 = @backend_timeout;\n }\n\n";
|
||||||
|
|
||||||
const NGINX_SOURCE_PROXY_BLOCK: &str = " # GitWorkshop follows the dashboard origin so LAN, Tailscale, FIPS, Tor,\n # hostnames and reverse proxies all use the connection that already works.\n location /app/archipelago-source/ {\n proxy_pass http://127.0.0.2:8337/;\n proxy_http_version 1.1;\n proxy_set_header Host $http_host;\n proxy_set_header Cookie $http_cookie;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header X-Forwarded-Prefix /app/archipelago-source;\n proxy_hide_header X-Frame-Options;\n add_header X-Frame-Options \"SAMEORIGIN\" always;\n add_header X-Content-Type-Options \"nosniff\" always;\n proxy_read_timeout 300s;\n }\n";
|
const NGINX_SOURCE_PROXY_BLOCK: &str = " # GitWorkshop follows the dashboard origin so LAN, Tailscale, FIPS, Tor,\n # hostnames and reverse proxies all use the connection that already works.\n location /app/archipelago-source/ {\n proxy_pass http://127.0.0.2:8337/;\n proxy_http_version 1.1;\n proxy_set_header Host $http_host;\n proxy_set_header Cookie $http_cookie;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header X-Forwarded-Prefix /app/archipelago-source;\n proxy_hide_header X-Frame-Options;\n add_header X-Frame-Options \"SAMEORIGIN\" always;\n add_header X-Content-Type-Options \"nosniff\" always;\n proxy_read_timeout 300s;\n }\n";
|
||||||
|
|
||||||
@@ -1783,6 +1783,14 @@ fn heal_missing_nostr_signer(content: &str) -> Option<String> {
|
|||||||
.then(|| content.replace(anchor, &format!("{}{}", NGINX_NOSTR_SIGNER_BLOCK, anchor)))
|
.then(|| content.replace(anchor, &format!("{}{}", NGINX_NOSTR_SIGNER_BLOCK, anchor)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keep both authenticated catalog endpoints on the backend in every vhost.
|
||||||
|
fn heal_node_catalog_route(content: &str) -> String {
|
||||||
|
content.replace(
|
||||||
|
"location /api/app-catalog {",
|
||||||
|
"location ~ ^/api/(?:app-catalog|node-app-catalog)$ {",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async fn patch_nginx_conf(path: &str) -> Result<bool> {
|
async fn patch_nginx_conf(path: &str) -> Result<bool> {
|
||||||
let content = fs::read_to_string(path)
|
let content = fs::read_to_string(path)
|
||||||
.await
|
.await
|
||||||
@@ -1795,7 +1803,8 @@ async fn patch_nginx_conf(path: &str) -> Result<bool> {
|
|||||||
|| content.contains(" location /electrs-status {");
|
|| content.contains(" location /electrs-status {");
|
||||||
let missing_app_catalog = content
|
let missing_app_catalog = content
|
||||||
.contains(" # DWN endpoints — peer access over Tor (no auth)")
|
.contains(" # DWN endpoints — peer access over Tor (no auth)")
|
||||||
&& !content.contains("location /api/app-catalog");
|
&& !content.contains("location /api/app-catalog")
|
||||||
|
&& !content.contains("location ~ ^/api/(?:app-catalog|node-app-catalog)$ {");
|
||||||
let missing_bitcoin_status = content.contains(" location /electrs-status {")
|
let missing_bitcoin_status = content.contains(" location /electrs-status {")
|
||||||
&& !content.contains("location /bitcoin-status");
|
&& !content.contains("location /bitcoin-status");
|
||||||
let missing_lnd_proxy = has_lnd_anchor && !content.contains("location /proxy/lnd/");
|
let missing_lnd_proxy = has_lnd_anchor && !content.contains("location /proxy/lnd/");
|
||||||
@@ -1816,7 +1825,9 @@ async fn patch_nginx_conf(path: &str) -> Result<bool> {
|
|||||||
let missing_source_proxy = heal_missing_source_proxy(&content).is_some();
|
let missing_source_proxy = heal_missing_source_proxy(&content).is_some();
|
||||||
let missing_source_prefix = heal_source_forwarded_prefix(&content).is_some();
|
let missing_source_prefix = heal_source_forwarded_prefix(&content).is_some();
|
||||||
let missing_nostr_signer = heal_missing_nostr_signer(&content).is_some();
|
let missing_nostr_signer = heal_missing_nostr_signer(&content).is_some();
|
||||||
|
let legacy_catalog_route = content.contains("location /api/app-catalog {");
|
||||||
if !missing_app_catalog
|
if !missing_app_catalog
|
||||||
|
&& !legacy_catalog_route
|
||||||
&& !missing_bitcoin_status
|
&& !missing_bitcoin_status
|
||||||
&& !missing_lnd_proxy
|
&& !missing_lnd_proxy
|
||||||
&& !missing_peer_content
|
&& !missing_peer_content
|
||||||
@@ -1833,7 +1844,7 @@ async fn patch_nginx_conf(path: &str) -> Result<bool> {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut patched = content.clone();
|
let mut patched = heal_node_catalog_route(&content);
|
||||||
|
|
||||||
if let Some(p) = heal_stale_web_search_block(&patched) {
|
if let Some(p) = heal_stale_web_search_block(&patched) {
|
||||||
patched = p;
|
patched = p;
|
||||||
@@ -2012,6 +2023,21 @@ async fn patch_nginx_conf(path: &str) -> Result<bool> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn catalog_routes_upgrade_both_vhosts_without_changing_access_guards() {
|
||||||
|
let old = "server { if ($guard) { return 404; } location /api/app-catalog { proxy_pass http://127.0.0.1:5678; } }\nserver { location /api/app-catalog { proxy_set_header Cookie $http_cookie; } }";
|
||||||
|
let fixed = super::heal_node_catalog_route(old);
|
||||||
|
assert_eq!(fixed.matches("location ~ ^/api/(?:app-catalog|node-app-catalog)$ {").count(), 2);
|
||||||
|
assert!(fixed.contains("if ($guard) { return 404; }"));
|
||||||
|
assert!(fixed.contains("proxy_set_header Cookie $http_cookie;"));
|
||||||
|
assert_eq!(super::heal_node_catalog_route(&fixed), fixed);
|
||||||
|
let route = regex::Regex::new(r"^/api/(?:app-catalog|node-app-catalog)$").unwrap();
|
||||||
|
assert!(route.is_match("/api/node-app-catalog"));
|
||||||
|
assert!(route.is_match("/api/app-catalog"));
|
||||||
|
assert!(!route.is_match("/api/node-app-catalog/extra"));
|
||||||
|
assert!(!route.is_match("/api/unrelated"));
|
||||||
|
}
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -849,10 +849,19 @@ pub async fn send_token_recoverable(
|
|||||||
} else {
|
} else {
|
||||||
let mut denominations = amount_to_denominations(amount_sats);
|
let mut denominations = amount_to_denominations(amount_sats);
|
||||||
denominations.extend(amount_to_denominations(excess));
|
denominations.extend(amount_to_denominations(excess));
|
||||||
let prepared = mint_client(data_dir, &binding.mint_url)
|
let client = mint_client(data_dir, &binding.mint_url).await?;
|
||||||
.await?
|
let prepared = client
|
||||||
.prepare_swap_at_least(&proofs, &denominations, amount_sats)
|
.prepare_swap_at_least(&proofs, &denominations, amount_sats)
|
||||||
.await?;
|
.await?;
|
||||||
|
// Establish recovery support before reserving or spending inputs.
|
||||||
|
// Newly derived outputs must not already exist at the mint.
|
||||||
|
let existing = client.restore_prepared_swap(&prepared).await.map_err(|_| {
|
||||||
|
anyhow::anyhow!("The mint could not verify payment recovery support; no funds spent")
|
||||||
|
})?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
existing.is_none(),
|
||||||
|
"Payment outputs already exist at the mint; no funds spent"
|
||||||
|
);
|
||||||
Request::Swap(prepared)
|
Request::Swap(prepared)
|
||||||
};
|
};
|
||||||
journal.prepare(binding.clone(), request).await?
|
journal.prepare(binding.clone(), request).await?
|
||||||
|
|||||||
@@ -991,13 +991,13 @@ impl MintClient {
|
|||||||
let echoed: Vec<BlindedMessageRequest> = serde_json::from_value(
|
let echoed: Vec<BlindedMessageRequest> = serde_json::from_value(
|
||||||
body.get("outputs")
|
body.get("outputs")
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or(serde_json::json!([])),
|
.context("Mint restore response omitted outputs")?,
|
||||||
)
|
)
|
||||||
.context("Failed to parse restored outputs")?;
|
.context("Failed to parse restored outputs")?;
|
||||||
let signatures: Vec<BlindSignature> = serde_json::from_value(
|
let signatures: Vec<BlindSignature> = serde_json::from_value(
|
||||||
body.get("signatures")
|
body.get("signatures")
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or(serde_json::json!([])),
|
.context("Mint restore response omitted signatures")?,
|
||||||
)
|
)
|
||||||
.context("Failed to parse restored signatures")?;
|
.context("Failed to parse restored signatures")?;
|
||||||
|
|
||||||
|
|||||||
@@ -717,6 +717,32 @@ async fn journal_recovers_lost_swap_reply_and_commits_change_once() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn recoverable_send_rejects_broken_recovery_before_reserving_or_spending() {
|
||||||
|
let mint = Mint::start(0, None).await;
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let mut wallet = WalletState::default();
|
||||||
|
wallet.mint_url = mint.url.clone();
|
||||||
|
wallet.add_proofs(&mint.url, vec![proof(ACTIVE, 8)]);
|
||||||
|
save_wallet(root.path(), &wallet).await.unwrap();
|
||||||
|
*mint.restore_reply.lock().unwrap() = Some(json!({}));
|
||||||
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
let context = "ab".repeat(32);
|
||||||
|
let error = send_token_recoverable(
|
||||||
|
root.path(), &id, EcashNetwork::Mainnet, &mint.url, 4, &context,
|
||||||
|
).await.unwrap_err();
|
||||||
|
assert!(error.to_string().contains("recovery support"));
|
||||||
|
assert_eq!(load_wallet(root.path()).await.unwrap().balance(), 8);
|
||||||
|
assert!(mint.requests.lock().unwrap().is_empty());
|
||||||
|
// Once the mint responds correctly the same unspent operation can proceed.
|
||||||
|
*mint.restore_reply.lock().unwrap() = None;
|
||||||
|
assert!(send_token_recoverable(
|
||||||
|
root.path(), &id, EcashNetwork::Mainnet, &mint.url, 4, &context,
|
||||||
|
).await.is_ok());
|
||||||
|
assert_eq!(mint.requests.lock().unwrap().len(), 1);
|
||||||
|
assert_eq!(load_wallet(root.path()).await.unwrap().balance(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn recoverable_send_reuses_original_operation_after_ambiguous_mint_response() {
|
async fn recoverable_send_reuses_original_operation_after_ambiguous_mint_response() {
|
||||||
let mint = Mint::start(0, None).await;
|
let mint = Mint::start(0, None).await;
|
||||||
@@ -916,6 +942,10 @@ async fn seed_restore_matches_points_and_rejects_foreign_or_duplicated_metadata(
|
|||||||
let mut wrong_amount = signature.clone();
|
let mut wrong_amount = signature.clone();
|
||||||
wrong_amount["amount"] = json!(8);
|
wrong_amount["amount"] = json!(8);
|
||||||
for response in [
|
for response in [
|
||||||
|
json!({}),
|
||||||
|
json!({"outputs":[]}),
|
||||||
|
json!({"signatures":[]}),
|
||||||
|
json!({"outputs":null,"signatures":[]}),
|
||||||
json!({"outputs":[output.clone(),output.clone()],"signatures":[signature.clone(),signature.clone()]}),
|
json!({"outputs":[output.clone(),output.clone()],"signatures":[signature.clone(),signature.clone()]}),
|
||||||
json!({"outputs":[foreign],"signatures":[signature.clone()]}),
|
json!({"outputs":[foreign],"signatures":[signature.clone()]}),
|
||||||
json!({"outputs":[output.clone()],"signatures":[wrong_keyset]}),
|
json!({"outputs":[output.clone()],"signatures":[wrong_keyset]}),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Node-scoped demo apps and persistent media
|
# Node-scoped demo apps and persistent media
|
||||||
|
|
||||||
Status: implementation under qualification; **not deployed or accepted**.
|
Status: managed demo deployed to the authorized node on6October; playback and
|
||||||
|
full lifecycle acceptance remain open. This is not a global catalog release.
|
||||||
|
|
||||||
The V4V demo is restricted to Yaya. The global catalog and other nodes must not
|
The V4V demo is restricted to Yaya. The global catalog and other nodes must not
|
||||||
receive an install button or banner for this prototype. Its manifest lives in
|
receive an install button or banner for this prototype. Its manifest lives in
|
||||||
@@ -104,3 +105,61 @@ node catalog, copied-data installation, lifecycle checks, physical companion
|
|||||||
checks and final dashboard cold-launch regression remain required. Deployment
|
checks and final dashboard cold-launch regression remain required. Deployment
|
||||||
writes to dev and Yaya are paused because another session installed a mining
|
writes to dev and Yaya are paused because another session installed a mining
|
||||||
candidate on both nodes; reconcile source before replacing either build.
|
candidate on both nodes; reconcile source before replacing either build.
|
||||||
|
|
||||||
|
## Signed managed installation — 6October
|
||||||
|
|
||||||
|
The operator signed the prepared node-only catalog. Pinned-root verification
|
||||||
|
passed, and canonical payload comparison matched the reviewed unsigned file.
|
||||||
|
The signer reordered JSON keys; raw-file reconstruction was not a valid payload
|
||||||
|
comparison. Neither catalog contents nor its audience were changed.
|
||||||
|
|
||||||
|
A fresh consistent backup preserved the original demo data/media and password
|
||||||
|
hash. The unused managed volumes were refreshed and verified byte-for-byte with
|
||||||
|
ownership/modes retained. An initial copy attempt found rsync unavailable; the
|
||||||
|
copy was completed using the standard library before catalog activation. The
|
||||||
|
original demo remains running on its original port with its volumes untouched.
|
||||||
|
|
||||||
|
Catalog activation preserved the backend binary, session key and all preexisting
|
||||||
|
app container identities/start times. The first public endpoint check exposed
|
||||||
|
missing nginx routing: the SPA returned HTML for the node catalog. Both dashboard
|
||||||
|
vhosts now route the two exact catalog endpoint names to the authenticated
|
||||||
|
backend. The original nginx configuration was backed up and nginx validation and
|
||||||
|
reload passed. Source template and upgrade repair are updated; their new backend
|
||||||
|
regression run is pending. Public management guards were not modified.
|
||||||
|
|
||||||
|
Both HTTP and HTTPS catalog checks return401 without authentication and200 with
|
||||||
|
the existing owner session. HTTPS diagnostics ignored the previously documented
|
||||||
|
legacy certificate trust problem; ordinary browser trust is not claimed fixed.
|
||||||
|
The signed response contains only the node-demo-v4v entry. The initial manual RPC
|
||||||
|
omitted required dockerImage and failed before creating the app; the corrected
|
||||||
|
normal install request used the exact image from the signed manifest.
|
||||||
|
|
||||||
|
The installed app reports running/ui-ready and its container health endpoint
|
||||||
|
returns200. A real deployed dashboard browser, with no catalog/package fixtures,
|
||||||
|
shows the Sovereign Music listing and launches the retained-password login screen
|
||||||
|
at390px with no app-gate screen. The operator login/song check has been requested.
|
||||||
|
Managed restart, playback controls, desktop/browser/companion acceptance and
|
||||||
|
audience/lifecycle checks remain open until their results are recorded.
|
||||||
|
|
||||||
|
Qualification update: normal managed restart returned to running/ui-ready with
|
||||||
|
container health200; the original demo remained running. Desktop1440px also
|
||||||
|
passes real listing/launch-to-login checks. The full isolated backend suite for
|
||||||
|
recovery preflight and catalog route migration passed1,785tests, zero failures,
|
||||||
|
five existing skips (`/tmp/archy-node-catalog-route-tests.log`).
|
||||||
|
|
||||||
|
### Operator changes and live player regression
|
||||||
|
|
||||||
|
The operator now explicitly requests Nostr sign-in and native signer integration
|
||||||
|
instead of the alpha password mode. This supersedes the earlier instruction to
|
||||||
|
omit native signing for this demo. Preserve cryptographic login and user consent;
|
||||||
|
qualify actual platform signer, cancellation, logout, browser and companion flows.
|
||||||
|
A newly qualified app image/manifest and node-only catalog signature are required.
|
||||||
|
|
||||||
|
The operator reports music continues after closing the deployed app but the native
|
||||||
|
bottom player does not appear. Earlier fixture tests do not close this real
|
||||||
|
installation regression. Test the actual signed catalog and package state, close,
|
||||||
|
controls and reopening the same frame before accepting the repair.
|
||||||
|
|
||||||
|
The requested promotion uses the final intro cymatic still as its background,
|
||||||
|
with a music/play graphic on the right or the app's For You banner treatment.
|
||||||
|
The previously captured login background does not satisfy this updated request.
|
||||||
|
|||||||
@@ -289,3 +289,10 @@ zero failures and five existing skips:
|
|||||||
`/tmp/archy-recoverable-send-executor-final-tests.log`.
|
`/tmp/archy-recoverable-send-executor-final-tests.log`.
|
||||||
No real payment or deployment was performed. Purchase RPC integration, durable
|
No real payment or deployment was performed. Purchase RPC integration, durable
|
||||||
seller settlement/receipt recovery and the end-to-end acceptance remain open.
|
seller settlement/receipt recovery and the end-to-end acceptance remain open.
|
||||||
|
|
||||||
|
Recovery preflight now rejects malformed or unsupported restore responses before
|
||||||
|
reserving or spending inputs, and refuses already-issued newly derived outputs.
|
||||||
|
Required restore fields cannot silently default to empty. The malformed-response
|
||||||
|
regression verifies unchanged spendable balance/no swap, then successful retry
|
||||||
|
when the mint responds correctly. Full isolated1,784passed initially; combined
|
||||||
|
catalog-route qualification1,785passed, zero failures/five existing skips.
|
||||||
|
|||||||
@@ -432,6 +432,17 @@ functional/UX review. These are additions to existing groups, not closed work.
|
|||||||
the selected relationship; describe exactly what changed.
|
the selected relationship; describe exactly what changed.
|
||||||
- Anchor removal buttons at card bottoms. Show an immediate per-action spinner,
|
- Anchor removal buttons at card bottoms. Show an immediate per-action spinner,
|
||||||
prevent duplicate submissions, and show an accurate completion/error toast.
|
prevent duplicate submissions, and show an accurate completion/error toast.
|
||||||
|
- Add a bottom-anchored Network map button to the Connected Nodes container,
|
||||||
|
linking directly to the network map screen on desktop and mobile.
|
||||||
|
- Include peer and trusted-node counts in the Connected Nodes top summary row,
|
||||||
|
with explicit labels and a compact responsive layout. Derive counts from the
|
||||||
|
authoritative relationship/trust state, update them automatically, and avoid
|
||||||
|
double-counting identities in the overall node total when categories overlap.
|
||||||
|
- Lay out these card-footer actions for mobile as well as desktop: maintain
|
||||||
|
bottom alignment within the card, clear labels, comfortable tap targets and
|
||||||
|
consistent spacing. Stack actions when needed instead of squeezing them;
|
||||||
|
prevent clipping, overlap and horizontal overflow. Check narrow phone widths,
|
||||||
|
enlarged text and loading states while preserving the existing design system.
|
||||||
- Measure and reduce removal latency. Verify whether an authenticated existing
|
- Measure and reduce removal latency. Verify whether an authenticated existing
|
||||||
FIPS connection is preferred; implement that priority where supported, with
|
FIPS connection is preferred; implement that priority where supported, with
|
||||||
bounded fallback and no weakening of identity or authorization checks.
|
bounded fallback and no weakening of identity or authorization checks.
|
||||||
|
|||||||
@@ -473,7 +473,7 @@ server {
|
|||||||
# so the browser doesn't hit CORS/CSP. Without this block nginx falls
|
# so the browser doesn't hit CORS/CSP. Without this block nginx falls
|
||||||
# through to the SPA index.html and the frontend gets HTML back instead
|
# through to the SPA index.html and the frontend gets HTML back instead
|
||||||
# of JSON.
|
# of JSON.
|
||||||
location /api/app-catalog {
|
location ~ ^/api/(?:app-catalog|node-app-catalog)$ {
|
||||||
proxy_pass http://127.0.0.1:5678;
|
proxy_pass http://127.0.0.1:5678;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -1380,7 +1380,7 @@ server {
|
|||||||
# so the browser doesn't hit CORS/CSP. Without this block nginx falls
|
# so the browser doesn't hit CORS/CSP. Without this block nginx falls
|
||||||
# through to the SPA index.html and the frontend gets HTML back instead
|
# through to the SPA index.html and the frontend gets HTML back instead
|
||||||
# of JSON.
|
# of JSON.
|
||||||
location /api/app-catalog {
|
location ~ ^/api/(?:app-catalog|node-app-catalog)$ {
|
||||||
proxy_pass http://127.0.0.1:5678;
|
proxy_pass http://127.0.0.1:5678;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
Reference in New Issue
Block a user