From 5ac992ea5700cb771799222f90317067b6a5936d Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Wed, 14 Oct 2026 15:53:44 +0800 Subject: [PATCH] SPA --- docs/csp-apache.conf.md | 2 + docs/spa-apache-routing-follow-up.md | 374 +++++++++++++++++++++++++++ 2 files changed, 376 insertions(+) create mode 100644 docs/spa-apache-routing-follow-up.md diff --git a/docs/csp-apache.conf.md b/docs/csp-apache.conf.md index b92563d0..75130ab0 100644 --- a/docs/csp-apache.conf.md +++ b/docs/csp-apache.conf.md @@ -6,6 +6,8 @@ Adjust `report-uri` if your API base path or host differs (UAT example below; PR See also: +- [spa-apache-routing-follow-up.md](./spa-apache-routing-follow-up.md) — SPA deep links (`/paymentPage/callback`, etc.) and Apache reverse-proxy setup +- [csp-api-logout-404-follow-up.md](./csp-api-logout-404-follow-up.md) — keep `/api/*` off SPA `index.html` fallback - [permissions-policy-apache.conf.md](./permissions-policy-apache.conf.md) — Permissions-Policy reporting via the same `/api/csp-report` URL - [csp-report-review-2026-09-22.md](./csp-report-review-2026-09-22.md) — latest PROD report review (`All_CSP.txt`) - [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — earlier PROD + UAT report review and add/remove summary diff --git a/docs/spa-apache-routing-follow-up.md b/docs/spa-apache-routing-follow-up.md new file mode 100644 index 00000000..d6efba58 --- /dev/null +++ b/docs/spa-apache-routing-follow-up.md @@ -0,0 +1,374 @@ +# Follow-up: SPA routing on Apache (`/paymentPage/callback` and deep links) + +Payment gateway redirects and bookmarked URLs hit paths like +`/paymentPage/callback?TRANSACTION_ID=...&RECEIPT_TOKEN=...`. In Chrome +DevTools, the **document** request often shows **404 Not Found**, while +the payment callback logic still runs (console logs from +`PaymentCallback.js`, success/cancel/fail UI). + +This document explains why that happens, how it relates to the current +Apache vhost (reverse proxy to `192.168.8.x:8080`), and what to change for +a proper SPA setup (document **200** + unchanged browser URL and query +string). + +Related docs: + +- [csp-apache.conf.md](./csp-apache.conf.md) — CSP headers on Apache +- [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) — noted document `status-code: 404` on SPA shell (including `/paymentPage/callback`) +- [csp-api-logout-404-follow-up.md](./csp-api-logout-404-follow-up.md) — `/api/*` must not be rewritten to `index.html` + +--- + +## Symptom + +| Observation | Typical meaning | +|---|---| +| `GET /paymentPage/callback?...` → **404** | Origin (or Apache) has no file at that path | +| Response `Server: GLD Server B`, `Content-Type: text/html` | Apache / front tier answered | +| Console still logs transaction data | React loaded (often via `index.html` body with wrong status) | +| Network shows red 404 on the **document** | Cosmetic for users if the shell loads; bad for monitoring, SEO, and some gateways | + +Example URL (card payment return from GCIS): + +```text +https://pnsps.gld.gov.hk/paymentPage/callback?TRANSACTION_ID=...&RECEIPT_TOKEN=... +``` + +--- + +## This is not a missing React route + +The client route exists and is wired to the callback page: + +| Piece | Location | +|---|---| +| Route `paymentPage/callback` | `src/routes/PublicUserRoutes.js` | +| Component | `src/pages/Payment/PaymentCallback.js` | +| Card `returnurl` | `src/pages/Payment/Card/index.js` (`/paymentPage/callback`) | + +React Router only runs **after** the browser loads the SPA shell +(`index.html` + JS). The **first** request is a full-page **GET** from +the payment gateway (or user bookmark). Apache / Tomcat must serve the +shell for that path; the router never sees a request that never delivered +HTML. + +Real callback work is **POST** to the backend after the page loads: + +| Step | API (via `ApiPathConst.js`) | +|---|---| +| 1 | `POST /api/payment/api/updatepayment` (`UPDATE_PAYMENT_API`) | +| 2 | `POST /api/payment/callback/status` (`PAYMENT_CALLBACK_STATUS_API`) | + +Backend (`PNSPS-backend`): `PaymentController` — `/api/updatepayment`, +`/callback/status` under the payment module (full path includes +`server.servlet.contextPath: /api` and controller prefix). + +--- + +## Current Apache pattern (DEV/UAT example) + +Typical vhost (`httpd-vhosts.conf`) for `pnspsdev.gld.gov.hk`: + +```apache +ProxyPass /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/ +ProxyPass /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/ +ProxyPass /api/ http://192.168.8.6:8080/api/ +ProxyPass / http://192.168.8.6:8080/ +ProxyPassReverse / http://192.168.8.6:8080/ +``` + +With only this: + +1. `GET /paymentPage/callback` is forwarded to `192.168.8.6:8080` as the + same path. +2. There is no file at `/paymentPage/callback` on the origin. +3. The origin returns **404** (sometimes still returning `index.html` in + the body — “SPA shell 404”). +4. React may boot, read `window.location.search`, and complete payment. + +So: **logic can work while the document status stays 404.** + +Spring Boot in this repo is **API-only** (`contextPath: /api`, static +resource mappings disabled in `application.yml`). The React build is +served separately (commonly Tomcat or static root on `:8080`). SPA +fallback must be configured on **Apache** and/or that static tier — not +in Spring MVC alone. + +--- + +## Design rules (all options) + +1. **`/api/`** — Always proxy to the backend. Never rewrite API paths to + `index.html`. (See [csp-api-logout-404-follow-up.md](./csp-api-logout-404-follow-up.md).) +2. **Other proxied apps** — Keep more specific `ProxyPass` prefixes + (`/paymentrecon-dept_web/`, `/paymentrecon-portal_web/`, etc.) **before** + the catch-all `/`. +3. **Deep links** — For `GET` requests that are not files and not API, + serve `index.html` with HTTP **200**. +4. **Query string** — Payment callback needs `?TRANSACTION_ID=...&RECEIPT_TOKEN=...` + in the **browser** URL. Use internal rewrite to `index.html`, not an + external redirect that drops params. +5. **Avoid** `ErrorDocument 404 /index.html` as the primary SPA strategy: + body may be the app while status remains **404** (same noise as today). + +--- + +## Option A (recommended): Apache serves the React build + +Deploy `npm run build` output to the Apache host. Proxy only API (and +other backends). Remove catch-all `ProxyPass /` to the app server for +the main site. + +```apache + + ServerName pnspsdev.gld.gov.hk + # ... SSLEngine, certificates ... + + DocumentRoot /var/www/pnsps/build + + + Options -Indexes +FollowSymLinks + AllowOverride None + Require all granted + # Apache 2.4.16+ — unknown paths → index.html, status 200 + FallbackResource /index.html + + + ProxyPreserveHost On + + ProxyPass /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/ + ProxyPassReverse /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/ + + ProxyPass /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/ + ProxyPassReverse /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/ + + ProxyPass /api/ http://192.168.8.6:8080/api/ + ProxyPassReverse /api/ http://192.168.8.6:8080/api/ + + # Do not ProxyPass / to 8080 for the main SPA when using DocumentRoot + +``` + +**Deploy:** After each frontend release, sync `build/` to +`/var/www/pnsps/build` (or your chosen path) on the Apache server. + +**Pros:** Clear split — static on Apache, API on Spring. Easy to reason +about 200 vs 404. + +**Cons:** Requires a deploy path for static files on the Apache tier. + +--- + +## Option B: Keep `ProxyPass /` — rewrite deep links to `index.html` before proxy + +Use when the React app must stay on `192.168.8.6:8080` and you only +control Apache. Rewrite eligible **GET** requests to `/index.html` on the +upstream; browser URL and query string stay unchanged (`[PT,QSA]`). + +Add **inside the same vhost**, after enabling `RewriteEngine` (you may +already have TRACE/TRACK rules): + +```apache +RewriteEngine on + +# Existing: deny TRACE / TRACK +RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK) +RewriteRule .* - [F] + +# Do not SPA-rewrite API or other proxied applications +RewriteCond %{REQUEST_URI} ^/api/ [OR] +RewriteCond %{REQUEST_URI} ^/paymentrecon-dept_web/ [OR] +RewriteCond %{REQUEST_URI} ^/paymentrecon-portal_web/ +RewriteRule ^ - [L] + +# Pass through static assets (Create React App uses /static/...) +RewriteCond %{REQUEST_URI} ^/static/ [OR] +RewriteCond %{REQUEST_URI} \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|map|json|txt|webmanifest)$ [NC] +RewriteRule ^ - [L] + +# SPA fallback: upstream receives GET /index.html; browser keeps /paymentPage/callback?... +RewriteCond %{REQUEST_METHOD} GET +RewriteCond %{REQUEST_URI} !^/index\.html$ +RewriteRule ^ /index.html [PT,QSA,L] + +# Existing ProxyPass (specific paths first) +ProxyPass /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/ +ProxyPassReverse /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/ + +ProxyPass /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/ +ProxyPassReverse /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/ + +ProxyPass /api/ http://192.168.8.6:8080/api/ +ProxyPassReverse /api/ http://192.168.8.6:8080/api/ + +ProxyPass / http://192.168.8.6:8080/ +ProxyPassReverse / http://192.168.8.6:8080/ +``` + +If rewrite and proxy order behaves differently on your Apache version, +try explicit proxy on the last rule: + +```apache +RewriteRule ^ /index.html [P,QSA,L] +``` + +(requires `mod_proxy` and correct module load order). + +**Pros:** No change to where the WAR/static tree lives on 8080. + +**Cons:** Rewrite + `ProxyPass` interaction must be tested per environment; +static file detection is by URL pattern, not upstream filesystem. + +--- + +## Option C: SPA fallback on Tomcat (origin `:8080`) + +If Apache cannot change, configure the tier that serves `/` on port 8080 +(Tomcat `RewriteValve`, filter, or error handling) so unknown **GET** +paths return `index.html` with **200**. Same idea as Options A/B; only the +layer moves. + +--- + +## Verification checklist + +Replace host with DEV/UAT/PROD as appropriate (`pnspsdev.gld.gov.hk`, +`pnsps.gld.gov.hk`, etc.). + +### 1. Payment callback document (main fix) + +```bash +curl -sS -D - -o /dev/null \ + "https://pnspsdev.gld.gov.hk/paymentPage/callback?TRANSACTION_ID=test&RECEIPT_TOKEN=test" +``` + +| Expected after fix | Before fix (typical) | +|---|---| +| **200**, `Content-Type: text/html` | **404**, HTML body may still be SPA | + +### 2. API still proxied (not rewritten to index) + +```bash +curl -sS -D - -o /dev/null -X POST \ + "https://pnspsdev.gld.gov.hk/api/payment/callback/status" \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +Expect a **JSON** error from Spring (4xx), **not** `index.html` HTML. +Status is not 404 from a missing static file. + +### 3. Static assets + +```bash +curl -sS -D - -o /dev/null \ + "https://pnspsdev.gld.gov.hk/static/js/main..chunk.js" +``` + +Use a real hash from the deployed `index.html`. Expect **200**. + +### 4. Other client routes + +```bash +curl -sS -D - -o /dev/null "https://pnspsdev.gld.gov.hk/login" +curl -sS -D - -o /dev/null "https://pnspsdev.gld.gov.hk/dashboard" +``` + +Expect **200** HTML shell when logged-out routes are publicly reachable +(may redirect in browser; curl shows initial response). + +### 5. Browser — card payment end-to-end + +1. Start card payment; complete or cancel on GCIS gateway. +2. DevTools → Network: document for `/paymentPage/callback?...` → **200**. +3. Network: `POST .../api/payment/api/updatepayment` and + `POST .../api/payment/callback/status` → success responses per business rules. +4. UI: success, cancel, or fail page from `PaymentCallback.js`. + +### 6. Apache reload + +```bash +sudo apachectl configtest +sudo systemctl reload apache2 +``` + +Use `httpd` / `apachectl` as on your GLD hosts. + +--- + +## Application behaviour (after shell loads) + +### Auth gate + +`paymentPage/callback` is under `PublicUserRoutes`, which is registered +only when the user is logged in as IND or ORG (`src/routes/index.js`). +If the session expires on the gateway, the user may hit `/login` instead +of the callback page. That is separate from the document 404; test with a +fresh login if callback APIs never fire. + +### FPS vs card + +| Flow | Return path | Route file | +|---|---|---| +| Card | `/paymentPage/callback` | `PublicUserRoutes.js` | +| FPS (PASG) | `/paymentPage/fps/fpscallback` | `PublicUserRoutes.js` (+ test route in `LoginRoutes.js`) | + +This follow-up focuses on **card** `returnurl` to `/paymentPage/callback`. + +### Code fragility (optional app fix, not Apache) + +`PaymentCallback.js` uses +`params.get("RECEIPT_TOKEN").replace("%3D", "=")` without a null check. +Missing `RECEIPT_TOKEN` throws before API calls. Harden separately if +gateways ever omit the param. + +--- + +## What not to do + +| Idea | Why | +|---|---| +| Add only a React route | Does not fix the first GET to Apache/Tomcat | +| `Redirect` callback URL to `/` | Loses query params unless carefully preserved | +| Rewrite `/api/` to `index.html` | Breaks payment POSTs, logout, login, etc. | +| Rely on 404 + `index.html` body | Payment may work; monitoring and CSP reports stay noisy | +| Change CSP for this | Not a CSP block; document status is HTTP routing | + +--- + +## Decision summary + +| Option | Where SPA fallback lives | Remove `ProxyPass /`? | +|---|---|---| +| A | Apache `DocumentRoot` + `FallbackResource` | Yes (for main site) | +| B | Apache `RewriteRule` → `/index.html` + existing proxy | No | +| C | Tomcat / origin on 8080 | No Apache change | + +**Recommended:** Option A if operations can publish `build/` to Apache; +Option B if the static tree must remain on `192.168.8.6:8080`. + +After implementation, `/paymentPage/callback` should show **200** on the +document request while behaviour of `PaymentCallback.js` and payment APIs +stays the same. + +--- + +## Code map (quick reference) + +| Piece | Location | +|---|---| +| Callback route | `PNSPS-frontend-MaterialUI/src/routes/PublicUserRoutes.js` | +| Callback UI + API calls | `PNSPS-frontend-MaterialUI/src/pages/Payment/PaymentCallback.js` | +| Card return URL | `PNSPS-frontend-MaterialUI/src/pages/Payment/Card/index.js` | +| API constants | `PNSPS-frontend-MaterialUI/src/utils/ApiPathConst.js` | +| Route auth wiring | `PNSPS-frontend-MaterialUI/src/routes/index.js` | +| Payment APIs | `PNSPS-backend/.../payment/web/PaymentController.java` | +| API context path | `PNSPS-backend/src/main/resources/application.yml` (`/api`) | + +--- + +## Change log (doc) + +| Date | Note | +|---|---| +| 2026-10-14 | Initial follow-up from payment callback 404 investigation and DEV vhost review |