# EHS Platform: SWI Assignments, JWT Access Control & Single Entry Point

This document consolidates technical discussion and recommendations for:

1. Why the SWI user page showed “no SWIs assigned”
2. Why child applications (SAT, PTW, SWI, Risk) can be opened directly by URL
3. What JWT does and does not protect
4. How to restrict access so apps are launched from the Master Dashboard
5. How to create a single public entry point in production

---

## Table of contents

1. [SWI user page: “No SWIs assigned to you”](#1-swi-user-page-no-swis-assigned-to-you)
2. [Direct URL access to SAT, PTW, SWI, Risk](#2-direct-url-access-to-sat-ptw-swi-risk)
3. [Restricting apps to Master Dashboard only](#3-restricting-apps-to-master-dashboard-only)
4. [Single public entry point (production)](#4-single-public-entry-point-production)





























8
5. [Quick reference](#5-quick-reference)

---

## 1. SWI user page: “No SWIs assigned to you”

### Symptom

On the SWI user portal, pages such as **Assigned SWIs**, **My SWIs**, or the home dashboard showed messages like:

- *“No SWIs have been assigned to you yet.”*
- *“No SWIs assigned to you yet.”*
- *“No SWIs assigned yet”* (completion panel)

…even when an admin had assigned SWI documents to that user.

### How SWI user pages load assignments

| Page | How assignments are loaded |
|------|----------------------------|
| **Assigned SWIs** | Via `useAssignmentNotifications` hook in `UserApp.jsx` (polls `/me/assignments`) |
| **My SWIs** | Direct call to `fetchMyAssignments()` → `GET /me/assignments` |
| **User Home** | Direct call to `fetchMyAssignments()` |
| **My Location SWIs** | `GET /me/location/documents` (location-based, not personal assignment) |

Backend query (`swi_backend/src/services/swiFormService.js`):

```sql
WHERE d.assigned_to_user_id = ?
```

Only documents **explicitly assigned to the logged-in user** appear on “My SWIs” and “Assigned SWIs”. Documents that exist at the user’s plant but are not assigned to them appear under **My Location SWIs** only.

### Root cause (bug): `user_id` vs `userId`

HSE SSO stores user identity from the JWT using **camelCase** (`userId`) via `shared/hse-auth/jwtPayload.js`:

```javascript
return {
  userId: p.userId ?? p.user_id ?? p.id,
  employeeName: ...,
  email: ...,
};
```

But SWI user pages were reading **snake_case** `user?.user_id`, which was always `undefined` for SSO users.

**Impact on Assigned SWIs page**

`UserApp.jsx` passed `userId` into `useAssignmentNotifications`:

```javascript
const userId = user?.user_id;  // always undefined for SSO
const notifications = useAssignmentNotifications(userId);
```

The hook bails out when `userId` is missing:

```javascript
if (userId == null || userId === "") {
  setAssignments([]);
  setLoading(false);
  return;  // never calls the API
}
```

Result: **Assigned SWIs always showed empty**, even when assignments existed.

**Impact on document list actions**

`UserSwiDocumentList` uses `currentUserId` to decide if the user can open a form:

```javascript
const isMine = userId != null && Number(doc.assigned_to_user_id) === userId;
const canAct = isMine && onOpenForm && onViewSubmission;
```

With wrong `currentUserId`, documents might list but **Open SWI** buttons would not appear.

### Fix applied

Added `authUserId(auth)` in `swi_frontend/src/utils/jwtPayload.js` to resolve id from `user_id`, `userId`, `id`, or the JWT token payload. Updated:

- `swi_frontend/src/pages/user/UserApp.jsx`
- `swi_frontend/src/pages/user/AssignedSwisPage.jsx`
- `swi_frontend/src/pages/user/MySwisPage.jsx`
- `swi_frontend/src/pages/user/MyLocationSwisPage.jsx`

### If assignments are still empty after the fix

Verify data and process, not just the frontend:

1. **Admin must assign the document** — Creating or “opening” a SWI is not enough. Use **Admin → Assign Document** and set the assignee.
2. **Same user id** — The assignee’s `user_id` in the database must match the id in the login JWT (`userId` / `user_id` claim).
3. **Check the API** — In browser DevTools → Network, confirm `GET /api/swi/me/assignments` returns rows for the logged-in user.
4. **Location vs personal** — Plant-level SWIs without `assigned_to_user_id` show under **My Location SWIs**, not **My SWIs**.

---

## 2. Direct URL access to SAT, PTW, SWI, Risk

### Question

When typing the link of any application (SAT, PTW, SWI, Risk) directly in the browser, access works. Shouldn’t JWT authorization prevent this?

### Answer: JWT authenticates identity, not navigation path

In this EHS platform, JWT is used to:

1. **Prove identity** on API requests (`Authorization: Bearer <token>`)
2. **Gate frontend routes** when there is **no valid token**

JWT does **not** block loading a SPA URL. Static HTML/JS bundles are always downloadable; protection is enforced in client-side route guards and API middleware.

### Current behavior per app

Child apps (SAT, PTW, SWI, Risk) follow this pattern:

```
Visit app URL
    │
    ├─ Token in URL (?token=...) from Master Dashboard
    │     → consumeTokenFromUrl() saves to localStorage (hse_auth)
    │
    ├─ Token already in localStorage from prior login
    │     → App opens directly ✓
    │
    └─ No token
          → redirectToHseLogin() → Master Dashboard /login
```

**SAT** (`SAT_Frontend/src/components/auth/ProtectedRoute.js`):

```javascript
if (!isAuthenticated()) {
  redirectToHseLogin();
}
```

**PTW / SWI** (`ptw_frontend/src/context/AuthContext.jsx`, `swi_frontend/src/context/AuthContext.jsx`):

```javascript
if (!getStoredAuth()?.token) {
  redirectToHseLogin();
}
```

### Why direct access works after first login

1. User logs in on **Master Dashboard** (`http://localhost:5174/login`).
2. JWT is stored in `localStorage` under key `hse_auth` (via `shared/hse-auth`).
3. Token lifetime is up to **7 days** (`JWT_EXPIRES_IN` on master auth).
4. Visiting `localhost:3000`, `localhost:5176`, etc. later still finds a valid token → app opens without going through Master again.

Master Dashboard only **passes** the token when launching from a project tile:

```javascript
// Master_Dashboard_Frontend/.../MasterDashboard.jsx
return `${base}?token=${encodeURIComponent(token)}`;
```

That is **SSO convenience**, not a lock on direct URLs.

### What JWT protects today vs what it does not

| Goal | Supported now? |
|------|----------------|
| Must be logged in (valid JWT) | Yes — no token → redirect to Master login |
| APIs require valid JWT | Yes — 401 clears auth and redirects |
| Cannot open app by URL after prior login | **No** — `hse_auth` persists in `localStorage` |
| Must launch only from Master Dashboard tile | **No** — no launch-token or audience (`aud`) check |

### SSO flow (reference)

See also: `docs/AUTH-AND-JWT-REFERENCE.md`

1. User signs in on **Master Dashboard** (sole HSE login UI).
2. Master stores JWT in `hse_auth`.
3. Master opens satellite apps with `?token=<jwt>`.
4. Child app reads URL token, persists to storage, strips query params.
5. Child app without token redirects to `VITE_HSE_LOGIN_URL` (default: Master `/login`).

**Unified storage:** `shared/hse-auth` → `localStorage` key `hse_auth`.

**Unified API:** `combined-server.mjs` on port `5012` (default) serves `/api/master/*`, `/api/sat/*`, `/api/swi/*`, `/api/ptw/*`, and ORAC `/api/*`.

---

## 3. Restricting apps to Master Dashboard only

“Only accessible from within Master Dashboard after login” is a **different requirement** than “must be logged in.”

A standard login JWT means: *this user is allowed to use the system.*  
It does **not** mean: *this user must have come from the Master Dashboard right now.*

Current tokens contain `userId`, `roles`, `roleIds`, etc. — no `aud` (audience), launch nonce, or referrer validation.

### Options (simplest → strongest)

#### Option A: Session-only tokens in child apps (quick win)

- Accept `?token=` from Master on launch.
- Store in **`sessionStorage`**, not `localStorage`.
- On direct visit with no URL token → redirect to Master login.
- Token cleared when browser tab/session ends.

**Limitation:** User can still bookmark `?token=...` until expiry. Not fully secure.

#### Option B: Short-lived “launch tokens” (recommended)

1. User clicks app tile in Master Dashboard.
2. Master backend: `POST /api/master/auth/app-launch` with `{ app: "sat" }`.
3. Backend returns a **short-lived JWT** (e.g. 60 seconds) with claims like `aud: "sat"`, `purpose: "app-launch"`.
4. Child app accepts only that token (or exchanges it for a session token).
5. Child app and **backends** reject long-lived login JWTs used for direct navigation.

Backends must verify `aud` / `purpose` on every request — frontend-only checks are bypassable.

#### Option C: Single public entry point (infra)

- Expose only Master Dashboard hostname on the public internet.
- Child apps on internal URLs or path-based routing behind reverse proxy.
- Firewall blocks direct access to ports `3000`, `5173`–`5176`.

See [Section 4](#4-single-public-entry-point-production).

#### Option D: Shared parent domain + HttpOnly cookie (strongest SPA pattern)

- Login sets **HttpOnly, Secure, SameSite** cookie on `.yourcompany.com`.
- Child apps on subdomains or paths read session via cookie (not `localStorage`).
- Optional: iframe + `postMessage` token handoff from Master.

#### What not to rely on alone

- `document.referrer` checks — easily bypassed, unreliable.
- Hiding URLs in the UI — security through obscurity only.

### Practical recommendation

| Layer | Action |
|-------|--------|
| Short term | Session-only storage in child apps + redirect if no `?token=` on first load |
| Proper fix | Master-issued short-lived launch JWT with `aud` per app; backends enforce |
| Production | One public URL (Master); child apps not directly reachable from outside |

---

## 4. Single public entry point (production)

A **single public entry point** means users and the internet only see **one hostname** (e.g. `https://ehs.yourcompany.com`). SAT, PTW, SWI, Risk, and the API are **not** exposed on separate public ports.

The repo is already unified on the **API** side via `combined-server.mjs`. Frontends still run on separate dev ports in local development.

### Target architecture

```
Internet
   │
   ▼
┌─────────────────────────────────────────────┐
│  Reverse proxy (Nginx / Traefik / Caddy)      │  ← ONLY public (443)
│  https://ehs.yourcompany.com                │
└─────────────────────────────────────────────┘
   │              │              │
   ▼              ▼              ▼
/  (login)    /sat/ /ptw/…    /api/*
Master UI     child UIs       combined-server.mjs
(static)      (static)        (Node, internal only)
```

| Public path | Serves |
|-------------|--------|
| `/` | Master Dashboard (login + launcher) |
| `/sat/` | SAT frontend build |
| `/risk/` | Risk Assessment frontend build |
| `/swi/` | SWI frontend build |
| `/ptw/` | PTW frontend build |
| `/api/` | Unified API (`combined-server.mjs`) |

**Firewall:** Open only **443** (and 80 → redirect to HTTPS). Block `3000`, `5173`–`5176`, `5012` from the public internet.

### Step 1 — Reverse proxy (Nginx example)

```nginx
server {
  listen 443 ssl http2;
  server_name ehs.yourcompany.com;

  # Master Dashboard
  location / {
    root /var/www/ehs/master;
    try_files $uri $uri/ /index.html;
  }

  location /sat/ {
    alias /var/www/ehs/sat/;
    try_files $uri $uri/ /sat/index.html;
  }

  location /risk/ {
    alias /var/www/ehs/risk/;
    try_files $uri $uri/ /risk/index.html;
  }

  location /swi/ {
    alias /var/www/ehs/swi/;
    try_files $uri $uri/ /swi/index.html;
  }

  location /ptw/ {
    alias /var/www/ehs/ptw/;
    try_files $uri $uri/ /ptw/index.html;
  }

  location /api/ {
    proxy_pass http://127.0.0.1:5012;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}
```

Run `combined-server.mjs` bound to **localhost only** (not `0.0.0.0:5012` on a public interface).

### Step 2 — Configure frontends for subpaths

Apps today assume root origin (`http://localhost:5175/`). Under one host they need a **base path**.

**Vite apps** (Master, SWI, PTW, Risk) — `vite.config.js`:

```javascript
export default defineConfig({
  base: "/swi/",  // or /ptw/, /risk/, / for Master
});
```

**React Router:**

```jsx
<BrowserRouter basename="/swi">
```

**CRA (SAT)** — `package.json`:

```json
"homepage": "/sat"
```

Rebuild all frontends; deploy `dist/` / `build/` to proxy static paths.

### Step 3 — Master Dashboard env (production)

```env
VITE_SAT_URL=/sat
VITE_RISK_ASSESSMENT_URL=/risk
VITE_SWI_URL=/swi
VITE_PTW_URL=/ptw
VITE_HSE_LOGIN_URL=https://ehs.yourcompany.com/login
```

Launch URLs become same-origin, e.g. `/sat/dashboard?token=...`.

### Step 4 — Child app env (production)

```env
VITE_MASTER_DASHBOARD_URL=https://ehs.yourcompany.com
VITE_HSE_LOGIN_URL=https://ehs.yourcompany.com/login
VITE_API_BASE_URL=/api/swi   # or /api/ptw, /api/sat — relative to same origin
```

Update `combined-server.mjs` `ALLOWED_ORIGINS` / CORS to the single production origin instead of many localhost ports.

### Step 5 — Deploy flow

1. Build all frontends.
2. Run `node combined-server.mjs` internally (PM2, Docker, systemd).
3. Nginx serves static builds + proxies `/api/`.
4. DNS: one A record → proxy server.

**Docker Compose sketch:**

```yaml
services:
  api:
    build: .
    command: node combined-server.mjs
    expose: ["5012"]    # not published to host

  nginx:
    image: nginx:alpine
    ports: ["443:443"]
    volumes:
      - ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf
      - ./deploy/master:/var/www/ehs/master
      - ./deploy/sat:/var/www/ehs/sat
      # ... other static builds
    depends_on: [api]
```

Only `nginx` publishes port 443.

### Step 6 — Optional: serve static apps from Node

Extend `combined-server.mjs` with `express.static()` for each built app under `/sat`, `/swi`, etc., with Nginx only in front of that one Node process. Simpler for small deployments.

### What single entry point achieves vs does not

| After single entry point | Result |
|--------------------------|--------|
| User visits `https://ehs.com/ptw/` | Works if they have a valid JWT |
| User visits `http://server:5176` | **Fails** — port not public |
| User bookmarks old dev URL | **Fails** in production |

Single entry point **hides** child URLs and ports. It does **not** stop a logged-in user from opening `/ptw/` directly once they know the path. Combine with launch tokens or session-only auth for stricter control.

### Local development

Keep `npm run dev` with multiple ports for HMR, or add local Nginx on `localhost:8080` mirroring production paths. Production should use the single-host model.

### Implementation checklist

- [ ] Add `base` / `basename` / `homepage` to all five frontends
- [ ] Change `VITE_*_URL` env vars to same-origin paths
- [ ] Build all frontends; deploy static assets behind one proxy
- [ ] Run `combined-server.mjs` internally; proxy `/api/` only
- [ ] Lock down firewall so only 443 is public
- [ ] (Recommended) Add launch-token or session-only auth

---

## 5. Quick reference

### Dev URLs (current)

| App | Folder | Typical dev URL |
|-----|--------|-----------------|
| Master Dashboard | `Master_Dashboard_Frontend/` | `http://localhost:5174` |
| SAT | `SAT_Frontend/` | `http://localhost:3000/dashboard` |
| Risk Assessment | `Risk_Assessment_Frontend/` | `http://localhost:5173` |
| SWI | `swi_frontend/` | `http://localhost:5175` |
| PTW | `ptw_frontend/` | `http://localhost:5176` |
| Unified API | `combined-server.mjs` | `http://localhost:5012` |

### Key files

| Topic | Location |
|-------|----------|
| HSE auth storage & SSO | `shared/hse-auth/` |
| Master app launcher URLs | `Master_Dashboard_Frontend/src/components/features/master-dashboard/MasterDashboard.jsx` |
| SWI assignment API | `swi_backend` → `GET /api/swi/me/assignments` |
| SWI user id fix | `swi_frontend/src/utils/jwtPayload.js` → `authUserId()` |
| Unified API server | `combined-server.mjs` |
| Auth reference | `docs/AUTH-AND-JWT-REFERENCE.md` |
| App boundaries | `docs/APPS-AND-API-BOUNDARIES.md` |
| Optimization (lean dev, bundles) | `docs/EHS-PROJECT-OPTIMIZATION-GUIDE.md` |

### Run full stack locally

```bash
npm run dev
```

From repo root — starts API, Master, SAT, ORAC UI, SWI, PTW concurrently.

For day-to-day work on one app, prefer API + that UI only — see **`EHS-PROJECT-OPTIMIZATION-GUIDE.md`** § Development workflow.

---

*Document generated from engineering discussion covering SWI assignment display, JWT/SSO access patterns, and production entry-point architecture.*
