cff52b4b9e
- Travel dates: TopBar chip + popover (outbound/return/travellers); sent to Grok prompt; itinerary.needsTravelDates drives a nudge banner; cache and prefetch ledger invalidate when dates change - Sea crossings: CrossingOption schema (Eurotunnel, DFDS, P&O, Brittany, Stena Line); CrossingSwapBlock under tunnel/ferry/crossing stops with trip-impact deltas and Book links; prompt requires 3-5 real options for every UK ↔ mainland route; picking a crossing triggers silent re-plan - Tesla in-car polish: UA + heuristic detection sets <html class="incar">; CSS overrides kill backdrop-filter, scale fonts, enforce 44px tap targets, disable hover flicker; geolocation + reverse geocode + crosshair button inside the From input; up/down arrow reorder buttons replace touch-broken HTML5 drag-and-drop - Tesla Fleet API stub: /.well-known/appspecific/com.tesla.3p.public-key.pem served from TESLA_FLEET_PUBLIC_KEY for partner domain verification; OAuth callback + vehicle_data stub return 503 until partner approval - Dockerfile + .dockerignore for Dokku deployment; server now serves client/dist in production
73 lines
3.6 KiB
TypeScript
73 lines
3.6 KiB
TypeScript
import { Router } from 'express';
|
|
import { env } from '../config/env.js';
|
|
import { createLogger } from '../lib/logger.js';
|
|
|
|
const log = createLogger('tesla');
|
|
const router = Router();
|
|
|
|
// ─── Domain verification ────────────────────────────────────────────────────
|
|
// Tesla fetches this path to confirm you own the domain registered with the
|
|
// Fleet API partner account. The body must be the EXACT PEM the partner key
|
|
// is registered with (the EC public key from your prime256v1 keypair).
|
|
//
|
|
// Set TESLA_FLEET_PUBLIC_KEY in Dokku config to the full PEM contents —
|
|
// including the BEGIN/END lines. Multi-line env vars work fine with Dokku
|
|
// when set via `dokku config:set roadtrip TESLA_FLEET_PUBLIC_KEY="$(cat key.pem)"`.
|
|
router.get('/.well-known/appspecific/com.tesla.3p.public-key.pem', (_req, res) => {
|
|
if (!env.tesla.publicKey) {
|
|
log.warn('Tesla public key requested but TESLA_FLEET_PUBLIC_KEY is empty');
|
|
res.status(404).type('text/plain').send('Public key not configured');
|
|
return;
|
|
}
|
|
res.type('application/x-pem-file');
|
|
res.set('Cache-Control', 'public, max-age=300');
|
|
res.send(env.tesla.publicKey);
|
|
});
|
|
|
|
// ─── OAuth callback (stub) ──────────────────────────────────────────────────
|
|
// Tesla redirects here with ?code=… after the user grants access. We exchange
|
|
// the code for a refresh token and store it against the logged-in user.
|
|
// Full implementation lands once partner approval is granted.
|
|
router.get('/api/auth/tesla/callback', async (req, res) => {
|
|
const { code, state, error } = req.query as Record<string, string | undefined>;
|
|
|
|
if (error) {
|
|
log.warn({ error, state }, 'Tesla OAuth error returned to callback');
|
|
res.redirect(`/?tesla_error=${encodeURIComponent(error)}`);
|
|
return;
|
|
}
|
|
if (!code) {
|
|
res.status(400).type('text/plain').send('Missing ?code from Tesla');
|
|
return;
|
|
}
|
|
if (!env.tesla.clientId || !env.tesla.clientSecret) {
|
|
log.warn('Tesla OAuth callback hit but client credentials not configured');
|
|
res.status(503).type('text/plain').send('Tesla integration not yet configured');
|
|
return;
|
|
}
|
|
|
|
// TODO once Tesla approves partner registration:
|
|
// 1. POST to https://auth.tesla.com/oauth2/v3/token with grant_type=authorization_code
|
|
// 2. Decode the id_token, persist refresh_token against req.auth.userId
|
|
// 3. Optional: enrol the vehicle via /api/1/partner_accounts/public_key
|
|
// 4. Redirect to / with a success flag
|
|
log.info({ codeLen: code.length, state }, 'Tesla OAuth callback received (stub)');
|
|
res.redirect('/?tesla_connected=pending');
|
|
});
|
|
|
|
// ─── Vehicle state (stub) ───────────────────────────────────────────────────
|
|
// Returns the current battery %, range and location for the connected vehicle.
|
|
// Until partner approval, returns 503 so the client can hide the integration UI.
|
|
router.get('/api/tesla/state', async (_req, res) => {
|
|
if (!env.tesla.clientId) {
|
|
res.status(503).json({ connected: false, reason: 'pending_partner_approval' });
|
|
return;
|
|
}
|
|
// TODO: look up the user's stored refresh token, exchange for access token,
|
|
// call https://fleet-api.prd.eu.vn.cloud.tesla.com/api/1/vehicles/{id}/vehicle_data,
|
|
// and shape the response into { battery, range, lat, lng, state, etc. }.
|
|
res.status(501).json({ connected: false, reason: 'not_implemented' });
|
|
});
|
|
|
|
export default router;
|