|
|
@@ -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 |
|
|
|
|
|
<VirtualHost *:443> |
|
|
|
|
|
ServerName pnspsdev.gld.gov.hk |
|
|
|
|
|
# ... SSLEngine, certificates ... |
|
|
|
|
|
|
|
|
|
|
|
DocumentRoot /var/www/pnsps/build |
|
|
|
|
|
|
|
|
|
|
|
<Directory /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 |
|
|
|
|
|
</Directory> |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
</VirtualHost> |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
**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.<hash>.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 | |