Tarkistettiin, että kaikki Google Sheetin ympäristömuuttujat löytyvät GCP Secret Managerista kaikissa ympäristöissä (inf, dev, tst, prd).
bnc-cpt-tesla-private-keybnc-cpt-tesla-public-key-pemGoogle Sheetissä oli 6 salaisuutta, joille ei ollut kontteja GCP:ssä:
- bnc-cpt-paypal-test-user-username
- bnc-cpt-paypal-test-user-password
- bnc-cpt-stripte-test-account-card-number
- bnc-cpt-stripte-test-account-card-validity-month
- bnc-cpt-stripte-test-account-card-validity-year
- bnc-cpt-stripte-test-account-card-crc-code
Huom: Stripe-salaisuuksissa on kirjoitusvirhe Google Sheetissä (stripte eikä stripe). Käytetään Google Sheetin muotoa, koska se on ainoa totuuden lähde.
| Ympäristö | Salaisuudet YAML | Salaisuudet GCP | Synkronoitu | Tila |
|---|---|---|---|---|
| inf | 41 | 41 | 39 OK | OK |
| dev | 41 | 41 | 39 OK | OK |
| tst | 41 | 41 | 39 OK | OK |
| prd | 41 | 41 | 39 OK | OK |
Kaikki ympäristöt synkronoitu onnistuneesti, 0 virhettä.
Local development and CI environments cannot perform real Tesla OAuth flows or Fleet API calls because they lack:
TESLA_CLIENT_ID and TESLA_CLIENT_SECRET — stored in GCP Secret Manager,
available only to Cloud Run services in dev/tst/prd.TESLA_REDIRECT_URI — registered with Tesla as
https://dev.api.carpulsetracker.com/api/v1/tesla/oauth/callback. Tesla will NOT
redirect to http://localhost:*.https://fleet-api.prd.na.vn.cloud.tesla.com)
may be unreachable or rate-limited from dev machines.Without these, the local API container (con-bnc-cpt-api) cannot:
1. Generate a valid Tesla OAuth authorization URL
2. Exchange an authorization code for tokens
3. Query the Fleet API for vehicle data
The .env.lcl already references a mock container that does not yet exist:
TESLA_AUTH_URL=http://con-bnc-cpt-tesla-mock:9100/oauth2/v3/authorize
TESLA_TOKEN_URL=http://con-bnc-cpt-tesla-mock:9100/oauth2/v3/token
TESLA_FLEET_API_BASE=http://con-bnc-cpt-tesla-mock:9100
We need a container on port 9100 that fulfills this contract.
A single Tesla Gateway container (con-bnc-cpt-tesla-gw) on port 9100 replaces the
planned con-bnc-cpt-tesla-mock. It operates in one of two modes controlled by the
TESLA_GW_MODE environment variable:
| Mode | Use Case | External Dependencies | Real Tesla Data |
|---|---|---|---|
| mock | CI, fast local dev | None | No |
| proxy | Manual local testing | Dev API on GCP | Yes |
Key architectural insight: Mock mode and proxy mode use completely different routing patterns:
TESLA_*_URL env vars point to the gateway, which impersonates Tesla./api/v1/tesla/* requests to the gateway, which forwards them to the dev API.The gateway impersonates Tesla. The local API treats it as the real Tesla API. No external network calls are made.
┌─────────────────────────────────────────────────────────────────────┐
│ MOCK MODE FLOW │
│ │
│ Browser (localhost:3333) │
│ │ │
│ │ 1. POST /api/v1/tesla/oauth/initiate {paymentId, plan} │
│ ▼ │
│ Vite Dev Proxy (/api -> localhost:8100) │
│ │ │
│ ▼ │
│ Local API (con-bnc-cpt-api:8100) │
│ │ │
│ │ 2. GET gateway:9100/oauth2/v3/authorize?... │
│ │ (TESLA_AUTH_URL points to gateway) │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ Tesla Gateway (con-bnc-cpt-tesla-gw) │ │
│ │ Port 9100 — MOCK MODE │ │
│ │ │ │
│ │ /oauth2/v3/authorize │ │
│ │ → Immediate redirect with mock code │ │
│ │ │ │
│ │ /oauth2/v3/token │ │
│ │ → Returns mock access_token │ │
│ │ │ │
│ │ /api/1/vehicles │ │
│ │ → Returns fake vehicle list │ │
│ │ VIN: 5YJ3E1EA8NF000001 │ │
│ │ Name: "Mock Tesla" │ │
│ │ Model: Model 3 │ │
│ │ │ │
│ │ /api/1/vehicles/{tag}/vehicle_data │ │
│ │ → Returns fake odometer, battery, etc │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ │ 3. Mock token + mock vehicle data returned │
│ ▼ │
│ Local API builds report from mock data │
│ │ │
│ │ 4. Redirect to localhost:3333?oauth=success&session_id=xxx │
│ ▼ │
│ Browser displays mock report │
│ │
│ *** No external network calls. No real credentials needed. *** │
└─────────────────────────────────────────────────────────────────────┘
The gateway proxies application-level Tesla API calls to the dev API on GCP Cloud Run. The dev API holds real Tesla credentials and performs real OAuth + Fleet API calls.
┌─────────────────────────────────────────────────────────────────────┐
│ PROXY MODE FLOW │
│ │
│ Browser (localhost:3333) │
│ │ │
│ │ 1. POST /api/v1/tesla/oauth/initiate {paymentId, plan} │
│ ▼ │
│ Vite Dev Proxy │
│ │ │
│ │ /api/v1/tesla/* → localhost:9100 (gateway) │
│ │ /api/* → localhost:8100 (local API) │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ Tesla Gateway (con-bnc-cpt-tesla-gw) │ │
│ │ Port 9100 — PROXY MODE │ │
│ │ │ │
│ │ Receives: POST /api/v1/tesla/oauth/ │ │
│ │ initiate {paymentId, plan} │ │
│ │ │ │
│ │ Injects: │ │
│ │ Header: X-Gateway-Key: <secret> │ │
│ │ Body: frontendOrigin: │ │
│ │ http://localhost:3333 │ │
│ │ │ │
│ │ Forwards to: │ │
│ │ https://dev.api.carpulsetracker.com │ │
│ │ /api/v1/tesla/oauth/initiate │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ │ 2. Dev API returns {authUrl: "https://auth.tesla.com/..."} │
│ ▼ │
│ Browser redirects to Tesla auth │
│ │ │
│ │ 3. User authenticates on auth.tesla.com │
│ ▼ │
│ Tesla redirects to registered TESLA_REDIRECT_URI: │
│ https://dev.api.carpulsetracker.com/api/v1/tesla/oauth/callback │
│ │ │
│ │ 4. Dev API exchanges code for token, fetches vehicles, │
│ │ builds report, reads frontend_origin from OAuth state │
│ ▼ │
│ Dev API redirects browser to: │
│ http://localhost:3333?oauth=success&session_id=xxx │
│ │ │
│ │ 5. GET /api/v1/tesla/report/{session_id} │
│ ▼ │
│ Vite Proxy → Gateway → dev.api.carpulsetracker.com │
│ │ │
│ │ 6. Returns stored report with real vehicle data │
│ ▼ │
│ Browser displays real Tesla report │
│ │
│ *** Gateway does NOT sit in the Tesla redirect path. *** │
│ *** Tesla redirects to dev API directly. *** │
└─────────────────────────────────────────────────────────────────────┘
Docker Network (all_lcl_docker)
┌──────────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ WUI │──API──│ API │────│ Gateway │ │
│ │ :3333 │ calls │ :8100 │ │ :9100 │ │
│ │ │ │ │ │ (mock) │ │
│ └──────────┘ └──────────┘ └────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
Data flow:
WUI API Gateway (mock)
│ │ │
│ POST /api/v1/tesla/ │ │
│ oauth/initiate │ │
│ {paymentId, plan} │ │
│─────────────────────────>│ │
│ │ GET /oauth2/v3/authorize │
│ │─────────────────────────>│
│ │ redirect + mock code │
│ │<─────────────────────────│
│ │ │
│ │ POST /oauth2/v3/token │
│ │ {code: "mock-code"} │
│ │─────────────────────────>│
│ │ {access_token: "mock-*"} │
│ │<─────────────────────────│
│ │ │
│ │ GET /api/1/vehicles │
│ │ Authorization: Bearer * │
│ │─────────────────────────>│
│ │ {response: [{id: 1, ..}]}│
│ │<─────────────────────────│
│ │ │
│ redirect to WUI │ │
│ ?oauth=success │ │
│ &session_id=xxx │ │
│<─────────────────────────│ │
│ │ │
│ GET /api/v1/tesla/ │ │
│ report/{session_id} │ │
│─────────────────────────>│ │
│ {vin, odometer, ...} │ │
│<─────────────────────────│ │
Docker Network GCP Cloud Run
┌─────────────────────────────┐ ┌───────────────────────┐
│ │ │ │
│ ┌──────┐ ┌──────────┐ │ │ ┌────────────────┐ │
│ │ WUI │ │ Gateway │──│────│──│ Dev API │ │
│ │ :3333│────│ :9100 │ │ │ │ (Cloud Run) │ │
│ │ │ │ (proxy) │ │ │ │ │ │
│ └──────┘ └──────────┘ │ │ └────────────────┘ │
│ │ │ │ │
│ ┌──────────┐ │ │ ▼ │
│ │ API │ (bypassed │ │ Tesla Fleet API │
│ │ :8100 │ for Tesla) │ │ GCP Secret Manager │
│ └──────────┘ │ │ │
└─────────────────────────────┘ └───────────────────────┘
Data flow:
WUI Gateway (proxy) Dev API Tesla
│ │ │ │
│ POST /api/v1/ │ │ │
│ tesla/oauth/ │ │ │
│ initiate │ │ │
│───────────────────>│ │ │
│ │ POST /api/v1/ │ │
│ │ tesla/oauth/ │ │
│ │ initiate │ │
│ │ + X-Gateway-Key │ │
│ │ + frontendOrigin │ │
│ │───────────────────>│ │
│ │ {authUrl: "https: │ │
│ │ //auth.tesla..."} │ │
│ │<───────────────────│ │
│ {authUrl} │ │ │
│<───────────────────│ │ │
│ │ │ │
│ Browser redirect ──│────────────────────│──────────────────>│
│ to auth.tesla.com │ │ User authorizes │
│ │ │<──────────────────│
│ │ │ callback with code│
│ │ │ │
│ │ │ Exchange code ───>│
│ │ │ <── tokens ───────│
│ │ │ GET vehicles ────>│
│ │ │ <── vehicle data ─│
│ │ │ │
│ Redirect to │ │ │
│ localhost:3333 │ │ │
│ ?oauth=success │ │ │
│<────────────────────────────────────────│ │
│ │ │ │
│ GET /api/v1/tesla/ │ │ │
│ report/{session_id}│ │ │
│───────────────────>│ │ │
│ │ GET /api/v1/tesla/ │ │
│ │ report/{session_id}│ │
│ │ + X-Gateway-Key │ │
│ │───────────────────>│ │
│ │ {vin, odometer,..} │ │
│ │<───────────────────│ │
│ {report data} │ │ │
│<───────────────────│ │ │
frontend_origin SolutionTesla's OAuth flow requires a registered TESLA_REDIRECT_URI. Ours is:
https://dev.api.carpulsetracker.com/api/v1/tesla/oauth/callback
This cannot be changed to http://localhost:* — Tesla only accepts pre-registered URIs.
After the dev API's callback handler processes the OAuth exchange (code → token →
vehicle data → report), it must redirect the browser back to the frontend. By default,
it redirects to https://dev.carpulsetracker.com. For local development, we need it to
redirect to http://localhost:3333.
Pass frontend_origin through the OAuth state:
Step 1: Gateway injects frontend_origin
─────────────────────────────────────────────────
Original request from WUI:
POST /api/v1/tesla/oauth/initiate
Body: {paymentId: "abc", plan: "premium"}
Gateway adds before forwarding to dev API:
Body: {paymentId: "abc", plan: "premium",
frontendOrigin: "http://localhost:3333"}
Header: X-Gateway-Key: <secret>
Step 2: Dev API stores frontend_origin in OAuth state
─────────────────────────────────────────────────
state_data = {
"paymentId": "abc",
"plan": "premium",
"frontend_origin": "http://localhost:3333"
}
# Stored server-side, keyed by random state token
Step 3: Tesla callback uses frontend_origin for redirect
─────────────────────────────────────────────────
Tesla redirects to:
dev.api.carpulsetracker.com/api/v1/tesla/oauth/callback
?code=REAL_AUTH_CODE&state=random_token
Dev API callback handler:
1. Validates state token
2. Exchanges code for access token
3. Fetches vehicle data from Fleet API
4. Stores report
5. Reads frontend_origin from state_data
6. Redirects browser to:
http://localhost:3333?oauth=success&session_id=xxx
(instead of https://dev.carpulsetracker.com?...)
frontend_origin is stored server-side in the OAuth state — it never appears in
URLs that could be tampered with by end users.localhost:3333 is a standard HTTP 302 — the browser follows it
naturally.┌──────────────┐ X-Gateway-Key: <secret> ┌──────────────┐
│ │──────────────────────────────>│ │
│ Gateway │ │ Dev API │
│ (local) │ Only accepts frontendOrigin │ (Cloud Run) │
│ │ when key is valid │ │
└──────────────┘ └──────────────┘
TESLA_GATEWAY_API_KEY) between the gateway and the dev API.bnc-cpt-tesla-gateway-api-key.X-Gateway-Key header on every proxied request.frontendOrigin parameter when the correct
gateway key is present. Without it, frontendOrigin is silently ignored and the
default frontend URL is used.Even with a valid gateway key, frontendOrigin is only accepted if it appears in the
dev API's CORS_ORIGINS allowlist:
CORS_ORIGINS: "https://dev.carpulsetracker.com,http://localhost:3333"
This double validation (gateway key + CORS allowlist) prevents both unauthorized gateway usage and arbitrary redirect targets.
The gateway container contains no Tesla credentials:
┌─────────────────────────────────────────────────┐
│ Gateway container secrets: │
│ │
│ TESLA_GW_API_KEY ........... gateway key only │
│ TESLA_CLIENT_ID ............ ✗ NOT present │
│ TESLA_CLIENT_SECRET ........ ✗ NOT present │
│ TESLA_PRIVATE_KEY .......... ✗ NOT present │
│ │
│ All sensitive operations happen on the dev API │
│ (token exchange, Fleet API calls with real │
│ tokens, vehicle data retrieval). │
└─────────────────────────────────────────────────┘
Existing slowapi rate limiting on the dev API's Tesla endpoints applies. Gateway
requests count against these limits. No additional rate limiting is needed.
Edit .env.lcl (or set in shell before docker compose up):
# ── Mock mode (default, for CI and fast local dev) ──────────
TESLA_GW_MODE=mock
# No other config needed. Gateway serves fake data.
# ── Proxy mode (for real Tesla testing) ─────────────────────
TESLA_GW_MODE=proxy
TESLA_GW_DEV_API_URL=https://dev.api.carpulsetracker.com
TESLA_GW_API_KEY=<value from GCP Secret Manager>
TESLA_GW_FRONTEND_ORIGIN=http://localhost:3333
| Variable | Default | Description |
|---|---|---|
TESLA_GW_MODE |
mock |
mock or proxy |
TESLA_GW_PORT_HOST |
9100 |
Host port mapping |
TESLA_GW_DEV_API_URL |
https://dev.api.carpulsetracker.com |
Target API (proxy mode only) |
TESLA_GW_API_KEY |
(empty) | Gateway auth key (proxy mode only) |
TESLA_GW_FRONTEND_ORIGIN |
http://localhost:3333 |
Injected into frontendOrigin |
TESLA_AUTH_URL |
http://con-bnc-cpt-tesla-gw:9100/... |
API's Tesla auth URL (mock mode) |
TESLA_TOKEN_URL |
http://con-bnc-cpt-tesla-gw:9100/... |
API's Tesla token URL (mock mode) |
TESLA_FLEET_API_BASE |
http://con-bnc-cpt-tesla-gw:9100 |
API's Fleet API base (mock mode) |
In proxy mode, the Vite dev server must split Tesla routes from other API routes:
vite.config.ts proxy rules:
/api/v1/tesla/* → VITE_TESLA_GW_URL (localhost:9100, the gateway)
/api/* → VITE_API_URL (localhost:8100, the local API)
This split is necessary because in proxy mode, Tesla requests bypass the local API entirely and go through the gateway to the dev API on GCP.
In mock mode, this split is not needed — all requests go to the local API, which talks
to the gateway internally via the TESLA_*_URL environment variables.
# docker-compose-app.yaml
tesla-gw:
image: img-tesla-gw
container_name: con-bnc-cpt-tesla-gw
pull_policy: never
build:
context: ./tesla-gw
dockerfile: Dockerfile
ports:
- "${TESLA_GW_PORT_HOST:-9100}:9100"
environment:
TESLA_GW_MODE: "${TESLA_GW_MODE:-mock}"
TESLA_GW_DEV_API_URL: "${TESLA_GW_DEV_API_URL:-https://dev.api.carpulsetracker.com}"
TESLA_GW_API_KEY: "${TESLA_GW_API_KEY:-}"
TESLA_GW_FRONTEND_ORIGIN: "${TESLA_GW_FRONTEND_ORIGIN:-http://localhost:3333}"
networks:
- all_lcl_docker
┌──────────────────────────────────────────────────────────────────┐
│ docker-compose stack │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ │
│ │ con-bnc-cpt- │ │ con-bnc-cpt- │ │ con-bnc-cpt- │ │
│ │ wui │ │ api │ │ tesla-gw │ │
│ │ :3333 │ │ :8100 │ │ :9100 │ │
│ │ (Vue 3) │ │ (FastAPI) │ │ (FastAPI) │ │
│ └───────┬────────┘ └───────┬────────┘ └─────┬─────────────┘ │
│ │ │ │ │
│ │ │ mock mode: │ │
│ │ all /api/* │ API ──────────>│ │
│ │──────────────────>│ (Tesla URLs) │ │
│ │ │ │ │
│ │ proxy mode: │ │ │
│ │ /api/v1/tesla/* │ │ │
│ │──────────────────────────────────────>│ │
│ │ │ │ ────> GCP │
│ │ /api/* (rest) │ │ │
│ │──────────────────>│ │ │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ │
│ │ con-bnc-cpt- │ │ con-bnc-cpt- │ │ con-bnc-cpt- │ │
│ │ db │ │ redis │ │ mailhog │ │
│ │ :5432 │ │ :6379 │ │ :8025 │ │
│ │ (PostgreSQL) │ │ (Redis) │ │ (Email) │ │
│ └────────────────┘ └────────────────┘ └────────────────────┘ │
│ │
│ Network: all_lcl_docker │
└──────────────────────────────────────────────────────────────────┘
The gateway exposes a /health endpoint:
GET http://localhost:9100/health
→ {"status": "healthy", "mode": "mock"} # or "proxy"
| Shell Action | Change |
|---|---|
check-dev-readiness.func.sh |
Replace con-bnc-cpt-tesla-mock → con-bnc-cpt-tesla-gw |
generate-docker-env-ports.func.sh |
Update Tesla endpoint resolution to use tesla_gw |
# lcl.env.yaml
tesla_gw:
port: 9100
host: "localhost"
protocol: "http"
url: "http://localhost:9100"
health_path: "/health"
container_name: "con-bnc-cpt-tesla-gw"
| Decision | Chosen | Alternative | Rationale |
|---|---|---|---|
| Separate container vs built-in proxy | Separate container | Add proxy logic to local API | Keeps API codebase clean; mock + proxy in one place |
| Frontend origin via OAuth state | Server-side state | Query param on callback URL | State is secure (server-side), no URL tampering possible |
| Gateway API key | Shared secret in header | mTLS, IP allowlist | Simple, sufficient for dev API; mTLS is overkill for internal tool |
| Mock mode as default | Mock (no deps) | Proxy (real data) | CI needs no external deps; developer opts into proxy explicitly |
| Vite proxy split in proxy mode | Separate /api/v1/tesla route |
Single API URL for all | Necessary because proxy mode bypasses local API for Tesla routes |