wiki: auto-compile 2026-05-06 (1 log(s), 157 articles)
This commit is contained in:
parent
95f2c8b03f
commit
79e1c6dcae
13 changed files with 606 additions and 6 deletions
2
.obsidian/plugins/hoarder-sync/data.json
vendored
2
.obsidian/plugins/hoarder-sync/data.json
vendored
|
|
@ -4,7 +4,7 @@
|
|||
"syncFolder": "Hoarder",
|
||||
"attachmentsFolder": "Hoarder/attachments",
|
||||
"syncIntervalMinutes": 60,
|
||||
"lastSyncTimestamp": 1778093404068,
|
||||
"lastSyncTimestamp": 1778097003807,
|
||||
"updateExistingFiles": false,
|
||||
"excludeArchived": true,
|
||||
"onlyFavorites": false,
|
||||
|
|
|
|||
|
|
@ -119,3 +119,6 @@ tags: [daily]
|
|||
- 20:37 | `cc-dashboard`
|
||||
- **Asked:** Developer requested calendar view with project time tracking, Azure DevOps sync, and daily task planner for work planning hub.
|
||||
- **Done:** Implemented calendar display, cache control fixes, and Live Feed bug fixes; deployed to production server successfully.
|
||||
- 21:05 (<1min) | `memory-compiler`
|
||||
- **Asked:** Create a knowledge compiler system to extract wiki articles from daily conversation logs.
|
||||
- **Done:** Compiled 2026-05-06 session into 5 new articles, updated 2 existing articles, and synchronized wiki metadata.
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ This 3-hop pattern works for hundreds of articles without vector search.
|
|||
| [[wiki/projects-overview/_index\|projects-overview/]] | All 42 Oliver Agency projects — grouped by server (optical-web-1, optical-dev, baic, box-cli) | 1 |
|
||||
| [[wiki/tech-patterns/_index\|tech-patterns/]] | Recurring tech stacks: FastAPI, React/Vite, Next.js, Azure AD, AI, Box, One2Edit, Redis/Celery, cost-tracker | 22 |
|
||||
| [[wiki/architecture/_index\|architecture/]] | Cross-cutting architectural patterns: Docker Compose, multi-agent AI, GCP timeout, RAG, hotfolder, optical-dev deploy, cost-tracker, new-project checklist, troubleshooting playbooks, ADR log, Cloud Run Jobs | 11 |
|
||||
| [[wiki/client-knowledge/_index\|client-knowledge/]] | Per-client notes for Ford, H&M, L'Oréal, Barclays, Ferrero, 3M | 6 |
|
||||
| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 103 |
|
||||
| [[wiki/client-knowledge/_index\|client-knowledge/]] | Per-client notes for Ford, H&M, L'Oréal, Barclays, Ferrero, 3M, BAIC | 7 |
|
||||
| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 107 |
|
||||
| [[wiki/connections/_index\|connections/]] | Cross-cutting insights linking 2+ concepts: FastAPI+Azure AD+Docker trinity, AI→cost-tracker, Apache+Vite basePath, GCP→REST polling, Box+hotfolder, Docker DNS+AdGuard, Celery prefork×faster_whisper memory stacking | 10 |
|
||||
| [[wiki/qa/_index\|qa/]] | Filed answers to queries (saved with `--file-back`) | 0 |
|
||||
| [[wiki/homelab/_index\|homelab/]] | Self-hosted infra: Proxmox install, IOMMU/PCI passthrough, hypervisor setup, budget builds, HP Elitedesk G3, Homarr API + Apps + Boards + Certificates + Integrations + Settings + Tasks + AdGuard + Clock + Docker Stats + Docker Integration + Download Client + Firewall + Proxmox Integration + Radarr + Readarr + Sonarr + Bookmarks + Calendar + Icons + App Widget + Weather + GitHub + Nextcloud + qBittorrent + RSS Feed + Speedtest Tracker + System Health Monitoring + System Resources + Services Map + Media Stack | 43 |
|
||||
|
|
|
|||
|
|
@ -20,12 +20,13 @@ Per-client notes for clients with 2+ active projects. Covers tech preferences, c
|
|||
| Barclays | Mod Comms, Banner Builder | FastAPI, React, Gemini, GCP, Docker | [[wiki/client-knowledge/barclays\|barclays]] |
|
||||
| Ferrero | AC Booking Tool | Node.js, Box API, CSV/OMG | [[wiki/client-knowledge/ferrero\|ferrero]] |
|
||||
| 3M | OMG Portal | Node.js, Vanilla JS, One2Edit proxy | [[wiki/client-knowledge/3m\|3m]] |
|
||||
| BAIC | BAIC Dashboard | React + Vite, FastAPI, Azure AD, rsync deploy | [[wiki/client-knowledge/baic\|baic]] |
|
||||
|
||||
## Single-Project Clients
|
||||
These clients have only one project — context lives in the project note:
|
||||
- **3M** → [[01 Projects/3m-portal/3M OMG Portal|3M OMG Portal]] — One2Edit proxy
|
||||
- **Barclays** → [[01 Projects/modcomms/Mod Comms|Mod Comms]] — AI proof review, GCP
|
||||
- **BAIC** → [[01 Projects/baic_dashboard/BAIC Dashboard|BAIC Dashboard]] — Make.com + Azure AD
|
||||
- **BAIC** → [[wiki/client-knowledge/baic|baic]] — rsync deploy, Azure AD SSO, FastAPI root_path, SSE auth
|
||||
- **PIMCO** → [[01 Projects/pimco-charts/PIMCO Chart Generator|PIMCO Charts]] — SVG chart generation
|
||||
- **Ferrero** → [[01 Projects/ferrero-ac-creator/Ferrero AC Booking|Ferrero AC Booking]] — Box API + CSV
|
||||
- **Solventum** → [[01 Projects/solventum-image-metadata/Solventum Image Metadata|Solventum Metadata]] — OpenAI + enterprise
|
||||
|
|
|
|||
56
wiki/client-knowledge/baic.md
Normal file
56
wiki/client-knowledge/baic.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
title: "BAIC — Client Knowledge"
|
||||
aliases: [baic-dashboard, baic-deploy]
|
||||
tags: [client-knowledge, baic, deploy, rsync, azure-ad]
|
||||
created: 2026-05-06
|
||||
updated: 2026-05-06
|
||||
---
|
||||
|
||||
# BAIC
|
||||
|
||||
BAIC is a Chinese automotive brand. Oliver Agency maintains the BAIC Dashboard — an internal analytics/reporting tool hosted on the main Oliver web server (`baic` SSH alias).
|
||||
|
||||
## Key Facts
|
||||
|
||||
- **SSH alias**: `baic` → `vadym.samoilenko@10.220.72.13` (same server as many Oliver projects — see [[wiki/infrastructure/server-baic]])
|
||||
- **URL**: `https://baic.oliver.solutions/dashboard/`
|
||||
- **Auth**: Azure AD / Microsoft SSO via MSAL
|
||||
- **Stack**: React + Vite (frontend) + FastAPI (backend)
|
||||
|
||||
## Deploy Process
|
||||
|
||||
> [!important] No git on server — deploy via rsync only
|
||||
|
||||
```bash
|
||||
# 1. Build locally
|
||||
npm run build
|
||||
|
||||
# 2. Sync dist/ to server (no git pull on server)
|
||||
rsync -avz --delete dist/ baic:/var/vhosts/baic.oliver.solutions/htdocs/dashboard/
|
||||
```
|
||||
|
||||
- Remote path: `/var/vhosts/baic.oliver.solutions/htdocs/dashboard/`
|
||||
- Backend systemd service: `baic_dashboard.service` (underscore, not hyphen)
|
||||
|
||||
```bash
|
||||
# Restart backend after Python changes
|
||||
ssh baic "sudo systemctl restart baic_dashboard.service"
|
||||
|
||||
# Check status
|
||||
ssh baic "sudo systemctl status baic_dashboard.service"
|
||||
```
|
||||
|
||||
## Known Gotchas
|
||||
|
||||
| Gotcha | Detail | Article |
|
||||
|--------|--------|---------|
|
||||
| FastAPI `root_path` route stripping | Routes must be registered WITHOUT `/dashboard` prefix | [[wiki/concepts/fastapi-root-path-route-stripping]] |
|
||||
| SSE needs `?token=` | `EventSource` can't set custom headers | [[wiki/concepts/sse-jwt-query-param]] |
|
||||
| SPA index.html must be no-cache | Old hashes cause blank screen after rebuild | [[wiki/concepts/spa-index-html-cache-control]] |
|
||||
| MS SSO IDs are not UUIDs | `ms-4n0T2x-...` format breaks UUID validators | [[wiki/concepts/microsoft-sso-non-uuid-ids]] |
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/infrastructure/server-baic]] — server overview (hosts 40+ domains, not just BAIC)
|
||||
- [[wiki/tech-patterns/azure-ad-msal-auth]] — MSAL SSO pattern
|
||||
- [[wiki/tech-patterns/react-vite-typescript]] — frontend stack
|
||||
|
|
@ -120,5 +120,10 @@
|
|||
| [[wiki/concepts/icloud-space-2-duplicate-files\|iCloud Concurrent Write: ' 2.md' Duplicate Files]] | When two devices write to the same iCloud Drive file at the same time, iCloud resolves the conflict | memory-compiler | 2026-05-05 |
|
||||
| [[wiki/concepts/one2edit-username-format\|One2Edit Username Format]] | One2Edit usernames are **email addresses**, not `firstname.lastname` handles. This trips up anyone g | 01 Projects/3m-portal | 2026-05-05 |
|
||||
| [[wiki/concepts/sqlite-not-null-as-boolean\|SQLite: IS NOT NULL AS Boolean]] | When a column contains sensitive data (e.g. a password hash), never select it directly to answer a y | sandbox | 2026-05-05 |
|
||||
| [[wiki/concepts/fastapi-root-path-route-stripping]] | Starlette strips `root_path` prefix before route matching — register `/healthz` not `/cc-dashboard/healthz` even when app is at that prefix | daily/2026-05-06.md | 2026-05-06 |
|
||||
| [[wiki/concepts/sse-jwt-query-param]] | Browser `EventSource` has no headers option — JWT must travel as `?token=` query param for SSE streams | daily/2026-05-06.md | 2026-05-06 |
|
||||
| [[wiki/concepts/spa-index-html-cache-control]] | Vite asset hashes change on rebuild — old cached `index.html` causes 404 on JS chunks; fix: `no-cache` on index.html, `immutable` on `/assets/` | daily/2026-05-06.md | 2026-05-06 |
|
||||
| [[wiki/concepts/microsoft-sso-non-uuid-ids]] | Microsoft SSO app IDs are not UUIDs (`ms-4n0T2x-...`) — UUID validation in routing/DB silently fails for SSO users | daily/2026-05-06.md | 2026-05-06 |
|
||||
|
||||
<!-- Articles added automatically by compile.py -->
|
||||
<!-- Format: | [[concepts/slug]] | One-line summary | daily/YYYY-MM-DD.md | date | -->
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ tags: [apache, proxy, mod_alias, gotcha, optical-dev, deployment]
|
|||
sources:
|
||||
- "daily/2026-04-30.md"
|
||||
created: 2026-04-30
|
||||
updated: 2026-04-30
|
||||
updated: 2026-05-06
|
||||
---
|
||||
|
||||
# Apache mod_alias Takes Priority Over mod_proxy
|
||||
|
|
@ -70,6 +70,30 @@ If ProxyPass rules appear correct but API calls return static HTML (or 404s from
|
|||
2. Verify the physical directory exists: `ls /var/www/html/<path>`
|
||||
3. If both exist, you have this conflict
|
||||
|
||||
### Fix 3: ProxyPassMatch with Explicit Prefix Stripping
|
||||
|
||||
When using a FastAPI app with `root_path` behind a conflicting Alias, `ProxyPassMatch` can strip the prefix manually:
|
||||
|
||||
```apache
|
||||
# Static files served by Alias
|
||||
Alias /cc-dashboard /var/www/html/cc-dashboard
|
||||
|
||||
# API proxy: capture everything AFTER /cc-dashboard/ and forward without prefix
|
||||
# FastAPI receives /api/... not /cc-dashboard/api/...
|
||||
ProxyPassMatch ^/cc-dashboard/api/(.*)$ http://127.0.0.1:8001/api/$1
|
||||
ProxyPassMatch ^/cc-dashboard/healthz$ http://127.0.0.1:8001/healthz
|
||||
|
||||
# RewriteRule [P,L] alternative (same effect)
|
||||
RewriteEngine On
|
||||
RewriteRule ^/cc-dashboard/api/(.*)$ http://127.0.0.1:8001/api/$1 [P,L]
|
||||
```
|
||||
|
||||
This pattern is required when:
|
||||
1. An `Alias` covers the same prefix (so simple `ProxyPass` never fires)
|
||||
2. The FastAPI app is configured with `root_path="/cc-dashboard"` (routes registered without prefix)
|
||||
|
||||
See [[wiki/concepts/fastapi-root-path-route-stripping]] for why FastAPI routes must NOT include the prefix.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/concepts/apache-proxypass-include-files-ignored]] — related Apache silent failure: ProxyPass in Include fragments ignored
|
||||
|
|
|
|||
90
wiki/concepts/fastapi-root-path-route-stripping.md
Normal file
90
wiki/concepts/fastapi-root-path-route-stripping.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
title: "FastAPI root_path Does Not Add a Prefix — Starlette Strips It Before Route Matching"
|
||||
aliases: [fastapi-root-path, starlette-root-path-routes, fastapi-behind-prefix]
|
||||
tags: [fastapi, starlette, routing, deployment, gotcha, apache, reverse-proxy]
|
||||
sources:
|
||||
- "daily/2026-05-06.md"
|
||||
created: 2026-05-06
|
||||
updated: 2026-05-06
|
||||
---
|
||||
|
||||
# FastAPI root_path Does Not Add a Prefix — Starlette Strips It Before Route Matching
|
||||
|
||||
When FastAPI is initialised with `root_path="/cc-dashboard"`, Starlette **strips** that prefix from the incoming URL before matching routes. Routes must therefore be registered **without** the prefix. Registering `/cc-dashboard/healthz` results in a permanent 404 — the router never sees the prefix in the path.
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- `root_path` is metadata for OpenAPI docs and proxy headers only — it is NOT prepended to route paths
|
||||
- Starlette removes `root_path` from the URL before route matching; routes see the stripped path
|
||||
- Register `/healthz`, not `/cc-dashboard/healthz`, even when the app is served at `/cc-dashboard/`
|
||||
- The Apache `ProxyPass /cc-dashboard/ http://127.0.0.1:8001/` strips the prefix before forwarding — FastAPI's `root_path` merely tells the app "I am mounted at this prefix" for docs generation
|
||||
- This behaviour is **not documented** on the FastAPI site; it is a Starlette ASGI mount convention
|
||||
|
||||
## Details
|
||||
|
||||
### The Counterintuitive Initialisation
|
||||
|
||||
```python
|
||||
# app is served by Apache at /cc-dashboard/
|
||||
app = FastAPI(root_path="/cc-dashboard")
|
||||
|
||||
# ✅ CORRECT — Starlette strips root_path, route matches /healthz
|
||||
@app.get("/healthz")
|
||||
async def healthz():
|
||||
return {"ok": True}
|
||||
|
||||
# ❌ WRONG — route registered as /cc-dashboard/healthz
|
||||
# but Starlette sees /healthz after stripping, so this never matches
|
||||
@app.get("/cc-dashboard/healthz")
|
||||
async def healthz_wrong():
|
||||
return {"ok": True}
|
||||
```
|
||||
|
||||
### How the Request Flows
|
||||
|
||||
```
|
||||
Browser → GET /cc-dashboard/healthz
|
||||
↓
|
||||
Apache ProxyPass /cc-dashboard/ http://127.0.0.1:8001/
|
||||
↓ (Apache strips /cc-dashboard prefix)
|
||||
Starlette receives → GET /healthz (root_path="/cc-dashboard" stored separately)
|
||||
↓
|
||||
Route matching: /healthz ← this is what must be registered
|
||||
```
|
||||
|
||||
### What root_path Actually Does
|
||||
|
||||
```python
|
||||
# root_path is used by Starlette for:
|
||||
# 1. OpenAPI/Swagger docs — generates correct absolute URLs in the schema
|
||||
# 2. url_for() — builds URLs with the prefix included
|
||||
# 3. Request.root_path — accessible in middleware for logging/tracing
|
||||
|
||||
app = FastAPI(root_path="/cc-dashboard")
|
||||
# Swagger UI served at /cc-dashboard/docs will use /cc-dashboard/openapi.json
|
||||
# url_for("healthz") → "/cc-dashboard/healthz"
|
||||
```
|
||||
|
||||
### Corresponding Apache Config
|
||||
|
||||
```apache
|
||||
# Apache strips /cc-dashboard before proxying
|
||||
ProxyPass /cc-dashboard/ http://127.0.0.1:8001/
|
||||
ProxyPassReverse /cc-dashboard/ http://127.0.0.1:8001/
|
||||
|
||||
# Trailing slash on BOTH sides is mandatory —
|
||||
# without it Apache passes the prefix intact → all routes 404
|
||||
```
|
||||
|
||||
> [!warning] ProxyPassMatch stripping variant
|
||||
> If using `ProxyPassMatch ^/cc-dashboard/(.*)$ http://127.0.0.1:8001/$1`, the `$1` capture group performs the same stripping manually. This is needed when you cannot use the simple `ProxyPass /prefix/ http://host/` form (e.g. when an `Alias` conflict exists — see [[wiki/concepts/apache-mod-alias-proxy-priority]]).
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/concepts/apache-mod-alias-proxy-priority]] — Alias/ProxyPass conflict that forces use of ProxyPassMatch
|
||||
- [[wiki/concepts/nextjs-basepath-auth-redirects]] — similar prefix-stripping gotcha in Next.js basePath
|
||||
- [[wiki/tech-patterns/fastapi-python-docker]] — standard Oliver FastAPI deployment pattern
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-05-06.md]] — BAIC dashboard deployment: routes registering with full prefix caused 404s
|
||||
126
wiki/concepts/microsoft-sso-non-uuid-ids.md
Normal file
126
wiki/concepts/microsoft-sso-non-uuid-ids.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
---
|
||||
title: "Microsoft SSO User IDs Are Not UUIDs — Azure Object IDs vs Custom ID Formats"
|
||||
aliases: [ms-sso-ids, azure-ad-non-uuid, microsoft-user-id-format]
|
||||
tags: [azure-ad, msal, sso, uuid, gotcha, auth, mongodb, fastapi]
|
||||
sources:
|
||||
- "daily/2026-05-06.md"
|
||||
created: 2026-05-06
|
||||
updated: 2026-05-06
|
||||
---
|
||||
|
||||
# Microsoft SSO User IDs Are Not UUIDs — Azure Object IDs vs Custom ID Formats
|
||||
|
||||
Microsoft SSO user identifiers from MSAL/Azure AD do not follow the UUID format. IDs can look like `ms-4n0T2x-aW4w1pO1OKYNf` — a custom alphanumeric format. Any code that validates user IDs as UUIDs (regex, Pydantic `UUID` type, MongoDB ObjectId coercion) will silently fail for SSO users, blocking login, profile creation, or data access.
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- Azure AD `objectId` IS a proper UUID (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) — but it may be transformed by the application into a custom format
|
||||
- App-level IDs derived from SSO tokens (e.g. with a `ms-` prefix) are **not** UUIDs
|
||||
- Any `re.match(UUID_PATTERN, user_id)` check, Pydantic `field: UUID`, or `ObjectId(user_id)` will raise/return None for non-UUID IDs
|
||||
- Use `str` for user ID fields — never coerce to UUID/ObjectId unless you fully control ID generation
|
||||
- The failure mode is silent: ID validation returns `None`, user is treated as "not found", and downstream code (email, notifications, access checks) never runs
|
||||
|
||||
## Details
|
||||
|
||||
### The ID Format Mismatch
|
||||
|
||||
```python
|
||||
# Standard UUID pattern used in many codebases
|
||||
UUID_PATTERN = re.compile(
|
||||
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
# Microsoft Azure AD objectId (direct) — IS a UUID ✓
|
||||
azure_object_id = "a3d8e2f1-4b5c-6d7e-8f9a-0b1c2d3e4f5a"
|
||||
|
||||
# App-transformed SSO ID — NOT a UUID ✗
|
||||
app_sso_id = "ms-4n0T2x-aW4w1pO1OKYNf"
|
||||
|
||||
# Validation fails silently
|
||||
if not UUID_PATTERN.match(app_sso_id):
|
||||
return None # SSO user is treated as "not found"
|
||||
```
|
||||
|
||||
### Where This Breaks
|
||||
|
||||
**FastAPI path parameters with UUID type:**
|
||||
```python
|
||||
# ❌ FastAPI raises 422 Unprocessable Entity for non-UUID IDs
|
||||
@app.get("/users/{user_id}")
|
||||
async def get_user(user_id: UUID): # rejects "ms-4n0T2x-..."
|
||||
...
|
||||
|
||||
# ✅ Use str, validate business rules separately
|
||||
@app.get("/users/{user_id}")
|
||||
async def get_user(user_id: str):
|
||||
...
|
||||
```
|
||||
|
||||
**Pydantic model UUID fields:**
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from uuid import UUID
|
||||
|
||||
# ❌ Pydantic v2 raises ValidationError for non-UUID string
|
||||
class UserRef(BaseModel):
|
||||
user_id: UUID # breaks for SSO users
|
||||
|
||||
# ✅ Keep as str
|
||||
class UserRef(BaseModel):
|
||||
user_id: str
|
||||
```
|
||||
|
||||
**MongoDB ObjectId coercion:**
|
||||
```python
|
||||
from bson import ObjectId
|
||||
|
||||
# ❌ ObjectId() raises InvalidId for non-24-char-hex strings
|
||||
user = await db.users.find_one({"_id": ObjectId(user_id)})
|
||||
|
||||
# ✅ Store user_id as a plain string field, not as _id ObjectId
|
||||
user = await db.users.find_one({"user_id": user_id})
|
||||
```
|
||||
|
||||
### Azure AD objectId vs App-Level IDs
|
||||
|
||||
| Source | Format | Example | Is UUID? |
|
||||
|--------|--------|---------|----------|
|
||||
| Azure AD `objectId` (raw) | UUID v4 | `a3d8e2f1-4b5c-...` | ✓ Yes |
|
||||
| MSAL `homeAccountId` | `<oid>.<tid>` | `a3d8e2f1...tenant123` | ✗ Partial |
|
||||
| App-transformed ID | Custom | `ms-4n0T2x-aW4w1pO1OKYNf` | ✗ No |
|
||||
| Local user IDs | UUID or ObjectId | Depends on stack | Controlled |
|
||||
|
||||
### Recommendation
|
||||
|
||||
```python
|
||||
# Store SSO users with a clear prefix and treat user_id as opaque string
|
||||
async def get_or_create_sso_user(token_claims: dict) -> dict:
|
||||
azure_oid = token_claims["oid"] # Azure objectId — UUID format
|
||||
|
||||
# Either use azure_oid directly (valid UUID), or apply a prefix
|
||||
# but be CONSISTENT across the codebase
|
||||
user_id = azure_oid # ✅ use raw Azure objectId — it IS a UUID
|
||||
|
||||
user = await db.users.find_one({"user_id": user_id})
|
||||
if not user:
|
||||
await db.users.insert_one({
|
||||
"user_id": user_id, # stored as str
|
||||
"email": token_claims["preferred_username"],
|
||||
"source": "azure_sso",
|
||||
})
|
||||
return user
|
||||
```
|
||||
|
||||
> [!warning] The silent failure pattern
|
||||
> When ID validation fails in middleware or a helper function and returns `None`, the caller often proceeds with `None` as a user reference. This causes `find_one({"user_id": None})` — which may return an unrelated document or None — rather than raising an auth error. Always fail loudly on invalid IDs.
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/concepts/azure-ad-yaml-allowlist-pattern]] — Azure AD auth + YAML authZ pattern used across Oliver projects
|
||||
- [[wiki/concepts/msal-vanilla-js-pkce]] — MSAL.js PKCE flow that produces the token claims
|
||||
- [[wiki/tech-patterns/azure-ad-msal-auth]] — Oliver standard SSO pattern
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-05-06.md]] — BAIC dashboard: SSO users with `ms-` prefixed IDs failing UUID validation in route handlers
|
||||
|
|
@ -5,7 +5,7 @@ tags: [security, middleware, regex, xss, fastapi, python]
|
|||
sources:
|
||||
- "daily/2026-04-29.md"
|
||||
created: 2026-04-29
|
||||
updated: 2026-04-29
|
||||
updated: 2026-05-06
|
||||
---
|
||||
|
||||
# Security Middleware Regex Matching JSON Keys Causes False Positives
|
||||
|
|
@ -101,6 +101,57 @@ def is_suspicious_value(value: str) -> bool:
|
|||
| `evaluate_result` | `eval` |
|
||||
| `expression_type` | `expression` |
|
||||
|
||||
## Command Injection Patterns: `\b` Word Boundary Is Mandatory
|
||||
|
||||
XSS patterns are not the only problem. **Command injection** regex patterns suffer the same false-positive issue when applied to raw JSON text, and the fix is different: word boundaries (`\b`) are required because HTML-context markers don't apply.
|
||||
|
||||
### The False Positive Table for Command Tokens
|
||||
|
||||
| Pattern (no `\b`) | False positive example |
|
||||
|-------------------|----------------------|
|
||||
| `sh\s+` | `"Josh Smith"` — the `sh ` in "Josh S..." matches |
|
||||
| `rm\s+` | `"Norm "` — `rm ` in "Norm " matches |
|
||||
| `nc\s+` | Any name containing `nc` followed by a space |
|
||||
| `wget\s+` | Field value `"get widgets"` — `get w` doesn't match, but `wget` as substring would |
|
||||
| `curl\s+` | `"security curl"` — substring match |
|
||||
|
||||
### Fix: Prefix All Command Tokens with `\b`
|
||||
|
||||
```python
|
||||
# ❌ BAD — matches substrings in names, field values, natural text
|
||||
COMMAND_INJECTION = re.compile(
|
||||
r'(sh\s+|rm\s+|nc\s+|wget\s+|curl\s+|bash\s+|python\s+|perl\s+)',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
# ✅ GOOD — \b ensures we only match at a word boundary
|
||||
COMMAND_INJECTION = re.compile(
|
||||
r'(\bsh\b|\brm\b|\bnc\b|\bwget\b|\bcurl\b|\bbash\b|\bpython\b|\bperl\b)',
|
||||
re.IGNORECASE
|
||||
)
|
||||
# Note: \b before the token, but also after — \bsh\b won't match "Josh" or "shell"
|
||||
```
|
||||
|
||||
### Combined Pattern (XSS + Command Injection, Value-Only Scan)
|
||||
|
||||
```python
|
||||
# Apply after parsing JSON — scan VALUES only
|
||||
XSS_PATTERNS = re.compile(
|
||||
r'(<\s*script|javascript\s*:|on\w+\s*=)',
|
||||
re.IGNORECASE
|
||||
)
|
||||
CMD_INJECTION = re.compile(
|
||||
r'(\bsh\b|\brm\s+-rf|\bnc\b\s+\S+\s+\d+|\bwget\b|\bcurl\b)',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
def is_suspicious_value(value: str) -> bool:
|
||||
return bool(XSS_PATTERNS.search(value) or CMD_INJECTION.search(value))
|
||||
```
|
||||
|
||||
> [!tip] Parse first, then scan
|
||||
> Word boundaries reduce false positives but don't eliminate them for common 2-letter tokens like `rm` and `nc`. The most robust approach is always: **parse JSON first, scan string values only** — as described in Fix 1 above.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/tech-patterns/xss-validation-false-positive-word-boundary]] — existing tech-pattern covering this same fix
|
||||
|
|
|
|||
110
wiki/concepts/spa-index-html-cache-control.md
Normal file
110
wiki/concepts/spa-index-html-cache-control.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
title: "SPA index.html Must Have no-cache Headers — Vite Asset Hash Mismatch After Rebuild"
|
||||
aliases: [spa-cache-control, vite-cache-broken-spa, index-html-no-cache]
|
||||
tags: [vite, spa, caching, apache, deployment, gotcha, react]
|
||||
sources:
|
||||
- "daily/2026-05-06.md"
|
||||
created: 2026-05-06
|
||||
updated: 2026-05-06
|
||||
---
|
||||
|
||||
# SPA index.html Must Have no-cache Headers — Vite Asset Hash Mismatch After Rebuild
|
||||
|
||||
After a Vite rebuild, JavaScript chunk filenames change (content-hash in filename). If the browser has cached the old `index.html`, it will request the old hash filenames — which no longer exist — and the SPA fails to load with 404 errors on JS/CSS chunks. The fix is to instruct browsers and CDNs to never cache `index.html` while allowing long-lived caching for the hashed asset files.
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- Vite generates filenames like `main-BfD92kXz.js` — hash changes on every rebuild
|
||||
- Old `index.html` cached in browser references old hash → 404 on JS chunks → blank page or partial load
|
||||
- `index.html` must always be served with `Cache-Control: no-cache, no-store, must-revalidate`
|
||||
- Hashed assets (`*.js`, `*.css` in `/assets/`) can be cached indefinitely (`max-age=31536000, immutable`)
|
||||
- The symptom is a white screen or console errors like `Failed to load module script` after deploying
|
||||
|
||||
## Details
|
||||
|
||||
### Apache: Two-Rule Cache Pattern
|
||||
|
||||
```apache
|
||||
<VirtualHost *:443>
|
||||
DocumentRoot /var/www/html/my-spa
|
||||
|
||||
# index.html — never cache (Vite rewrites it on every build)
|
||||
<Files "index.html">
|
||||
Header always set Cache-Control "no-cache, no-store, must-revalidate"
|
||||
Header always set Pragma "no-cache"
|
||||
Header always set Expires "0"
|
||||
</Files>
|
||||
|
||||
# Hashed assets — cache forever (hash in filename guarantees uniqueness)
|
||||
<LocationMatch "^/assets/.*\.(js|css|woff2?|png|svg|ico)$">
|
||||
Header always set Cache-Control "public, max-age=31536000, immutable"
|
||||
</LocationMatch>
|
||||
|
||||
# SPA fallback — all routes serve index.html
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ /index.html [L]
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
### Nginx Equivalent
|
||||
|
||||
```nginx
|
||||
server {
|
||||
root /var/www/html/my-spa;
|
||||
|
||||
# Never cache index.html
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
# Cache hashed Vite assets forever
|
||||
location /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Diagnosing the Problem
|
||||
|
||||
When users report "blank screen after deployment" or "old version still showing":
|
||||
|
||||
1. Open DevTools → Network → filter by `index.html`
|
||||
2. Check Response Headers for `Cache-Control`
|
||||
3. If `max-age > 0` or `Cache-Control` is absent — this is the bug
|
||||
4. Hard refresh (`Ctrl+Shift+R`) will load the new version, confirming cache as root cause
|
||||
5. Check console for `Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/html"` — this is the 404-as-HTML fallback
|
||||
|
||||
### Why Hashed Assets Can Have Infinite Cache
|
||||
|
||||
Vite's build output:
|
||||
```
|
||||
dist/
|
||||
index.html ← changes every build (references new hashes)
|
||||
assets/
|
||||
main-BfD92kXz.js ← hash in filename; content is immutable for this URL
|
||||
vendor-CqRt4mPn.js
|
||||
index-Dx8kLpWw.css
|
||||
```
|
||||
|
||||
Because the filename itself is the cache key and contains the content hash, old URLs are simply abandoned and new hashes take their place. There is no risk of stale content for hashed files.
|
||||
|
||||
> [!note] Same issue affects sub-path deployments
|
||||
> When serving a SPA at a prefix (e.g. `/dashboard/`), the `<Files "index.html">` directive must cover the actual served path. If using [[wiki/concepts/vite-prebuilt-subpath-workaround]] with `<base href>`, verify the same no-cache rule covers the entry point.
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/concepts/vite-prebuilt-subpath-workaround]] — Vite `<base href>` pattern for sub-path deployments
|
||||
- [[wiki/concepts/shell-static-deploy-patterns]] — static deploy scripts: cp, rsync, Apache reload
|
||||
- [[wiki/tech-patterns/react-vite-typescript]] — standard Oliver SPA stack
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-05-06.md]] — BAIC dashboard: blank screen after Vite rebuild; old index.html referencing non-existent chunk hashes
|
||||
128
wiki/concepts/sse-jwt-query-param.md
Normal file
128
wiki/concepts/sse-jwt-query-param.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
title: "SSE / EventSource Does Not Support Custom Headers — Pass JWT as Query Param"
|
||||
aliases: [sse-no-headers, eventsource-jwt, sse-auth-token]
|
||||
tags: [sse, eventsource, jwt, auth, browser, security, gotcha]
|
||||
sources:
|
||||
- "daily/2026-05-06.md"
|
||||
created: 2026-05-06
|
||||
updated: 2026-05-06
|
||||
---
|
||||
|
||||
# SSE / EventSource Does Not Support Custom Headers — Pass JWT as Query Param
|
||||
|
||||
The browser-native `EventSource` API cannot set custom HTTP headers. There is no `headers` option in the constructor. For JWT-authenticated SSE streams, the token must be appended as a query parameter (`?token=...`). This is a known browser limitation with no workaround short of switching to `fetch()` with a `ReadableStream`.
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- `new EventSource(url)` sends only cookies and browser-default headers — no way to add `Authorization: Bearer`
|
||||
- JWT must travel as a URL query parameter: `?token=<jwt>` for browser-native SSE
|
||||
- Query-param tokens are visible in server access logs — use short-lived tokens (< 60 s) and rotate them
|
||||
- Alternative: use `fetch()` + `ReadableStream` (SSE-over-fetch) which does support custom headers, but requires manual reconnect logic
|
||||
- Backend must accept both `Authorization` header (for regular API calls) and `?token=` (for SSE)
|
||||
|
||||
## Details
|
||||
|
||||
### The Problem
|
||||
|
||||
```javascript
|
||||
// ❌ This is the intuitive approach — but EventSource has no headers option
|
||||
const es = new EventSource("/stream", {
|
||||
headers: { Authorization: `Bearer ${token}` } // IGNORED — not a valid option
|
||||
});
|
||||
```
|
||||
|
||||
### Fix: Token as Query Parameter
|
||||
|
||||
```javascript
|
||||
// ✅ Pass token as query param
|
||||
const token = localStorage.getItem("access_token");
|
||||
const es = new EventSource(`/stream?token=${encodeURIComponent(token)}`);
|
||||
|
||||
es.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
es.onerror = (err) => {
|
||||
es.close();
|
||||
// Reconnect logic here
|
||||
};
|
||||
```
|
||||
|
||||
### FastAPI Backend: Accept Both Header and Query Param
|
||||
|
||||
```python
|
||||
from fastapi import Query, Header, HTTPException
|
||||
from typing import Optional
|
||||
|
||||
async def get_current_user_sse(
|
||||
token: Optional[str] = Query(None),
|
||||
authorization: Optional[str] = Header(None),
|
||||
):
|
||||
"""Auth handler that accepts JWT from header OR query param (for SSE)."""
|
||||
raw_token = None
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
raw_token = authorization.removeprefix("Bearer ")
|
||||
elif token:
|
||||
raw_token = token
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
return verify_jwt(raw_token) # your existing JWT verifier
|
||||
|
||||
@app.get("/stream")
|
||||
async def stream_events(
|
||||
request: Request,
|
||||
user = Depends(get_current_user_sse),
|
||||
):
|
||||
async def event_generator():
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
yield {"data": json.dumps({"status": "ok"})}
|
||||
await asyncio.sleep(2)
|
||||
|
||||
return EventSourceResponse(event_generator())
|
||||
```
|
||||
|
||||
### Alternative: fetch() + ReadableStream (Supports Headers)
|
||||
|
||||
```javascript
|
||||
// ✅ Full header support, but you must implement reconnect manually
|
||||
const response = await fetch("/stream", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const text = decoder.decode(value);
|
||||
// Parse SSE text format manually: "data: {...}\n\n"
|
||||
processSSEChunk(text);
|
||||
}
|
||||
```
|
||||
|
||||
### Security Considerations
|
||||
|
||||
| Approach | Token exposure | Reconnect | Browser support |
|
||||
|----------|---------------|-----------|-----------------|
|
||||
| `?token=` query param | Server logs, referrer headers | Automatic | Universal |
|
||||
| `fetch()` + ReadableStream | Headers only (safe) | Manual | Modern browsers |
|
||||
| Cookie (httpOnly) | Minimal | Automatic | Universal, requires CORS cookie config |
|
||||
|
||||
> [!tip] Prefer short-lived tokens for SSE
|
||||
> If using query param auth, generate a dedicated short-lived SSE token (60–300 s) via a separate endpoint. This limits the window of exposure if a token is leaked via logs.
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/concepts/websocket-react-token-guard]] — WebSocket token guard pattern (similar auth-before-connect problem)
|
||||
- [[wiki/concepts/double-submit-cookie-csrf]] — alternative cookie-based stateless auth that avoids the query-param exposure
|
||||
- [[wiki/concepts/zustand-async-hydration]] — Zustand hydration delay that can cause token=null on first SSE connect
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-05-06.md]] — BAIC dashboard live feed: EventSource rejecting Authorization header
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
|
||||
# Build Log
|
||||
|
||||
## [2026-05-06T21:00:00+01:00] compile | 2026-05-06.md
|
||||
- Source: daily/2026-05-06.md
|
||||
- Articles created: [[wiki/concepts/fastapi-root-path-route-stripping]], [[wiki/concepts/sse-jwt-query-param]], [[wiki/concepts/spa-index-html-cache-control]], [[wiki/concepts/microsoft-sso-non-uuid-ids]], [[wiki/client-knowledge/baic]]
|
||||
- Articles updated: [[wiki/concepts/security-middleware-json-key-regex]] (added command injection `\b` word boundary section with false-positive table for `sh`, `rm`, `nc`); [[wiki/concepts/apache-mod-alias-proxy-priority]] (added Fix 3: ProxyPassMatch stripping pattern for FastAPI-behind-prefix)
|
||||
- Index updates: [[wiki/concepts/_index]] (123→127); [[wiki/client-knowledge/_index]] (6→7, BAIC added to table); [[wiki/_master-index]] (concepts 103→127, client-knowledge 6→7)
|
||||
|
||||
## [2026-05-05T21:00:00+01:00] compile | 2026-05-05.md (pass 2)
|
||||
- Source: daily/2026-05-05.md
|
||||
- Articles created: [[wiki/concepts/icloud-space-2-duplicate-files]], [[wiki/concepts/one2edit-username-format]], [[wiki/concepts/sqlite-not-null-as-boolean]]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue