Jason Chuang 3 недель назад
Родитель
Сommit
5ac992ea57
2 измененных файлов: 376 добавлений и 0 удалений
  1. +2
    -0
      docs/csp-apache.conf.md
  2. +374
    -0
      docs/spa-apache-routing-follow-up.md

+ 2
- 0
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: 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 - [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-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 - [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — earlier PROD + UAT report review and add/remove summary


+ 374
- 0
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
<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 |

Загрузка…
Отмена
Сохранить