import { Router } from 'express'; import crypto from 'node:crypto'; import { env } from '../config/env.js'; import { createLogger } from '../lib/logger.js'; import { clearOwnerCookie, isOwnerAuthenticated, setOwnerCookie, verifyOwnerSecret, } from '../lib/ownerAuth.js'; const log = createLogger('owner'); const router = Router(); // ─── Device pair-code store (in-memory) ───────────────────────────────────── // 6-digit short codes minted by an already-logged-in owner session so a // touchscreen device (the Tesla in-car browser) can log in without retyping // the long OWNER_SECRET. 5-minute TTL, single use, rate-limited. const PAIR_CODE_TTL_MS = 5 * 60 * 1000; const pairCodes = new Map(); // IP → recent redeem attempt timestamps (oldest first). const redeemAttempts = new Map(); const MAX_REDEEM_PER_MIN = 8; function cleanPairCodes(): void { const now = Date.now(); for (const [k, v] of pairCodes) if (v.expiresAt < now) pairCodes.delete(k); } function rateLimited(ip: string): boolean { const now = Date.now(); const arr = (redeemAttempts.get(ip) || []).filter(t => now - t < 60_000); arr.push(now); redeemAttempts.set(ip, arr); return arr.length > MAX_REDEEM_PER_MIN; } router.get('/api/auth/owner/status', (req, res) => { res.json({ authenticated: isOwnerAuthenticated(req), required: !!env.ownerSecret, }); }); router.post('/api/auth/owner', (req, res) => { if (!env.ownerSecret) { res.status(503).json({ ok: false, reason: 'owner_auth_not_configured' }); return; } const { secret } = (req.body || {}) as { secret?: unknown }; if (!verifyOwnerSecret(secret)) { res.status(401).json({ ok: false, reason: 'invalid_secret' }); return; } setOwnerCookie(res); res.json({ ok: true }); }); router.post('/api/auth/owner/logout', (_req, res) => { clearOwnerCookie(res); res.json({ ok: true }); }); // ─── Pair code: mint one (requires owner session) ────────────────────────── router.post('/api/auth/owner/pair-code', (req, res) => { if (!isOwnerAuthenticated(req)) { res.status(401).json({ ok: false, reason: 'auth_required' }); return; } cleanPairCodes(); // Cap concurrent codes per host to a sane number. if (pairCodes.size > 32) { res.status(429).json({ ok: false, reason: 'too_many_codes' }); return; } let code: string; do { code = String(crypto.randomInt(100_000, 1_000_000)); } while (pairCodes.has(code)); pairCodes.set(code, { expiresAt: Date.now() + PAIR_CODE_TTL_MS }); res.json({ ok: true, code, expiresInS: Math.floor(PAIR_CODE_TTL_MS / 1000) }); }); // ─── Pair code: redeem on a new device ────────────────────────────────────── router.post('/api/auth/owner/pair-redeem', (req, res) => { const ip = (req.ip || (req.headers['x-forwarded-for'] as string) || 'unknown').toString(); if (rateLimited(ip)) { res.status(429).json({ ok: false, reason: 'rate_limited' }); return; } const { code } = (req.body || {}) as { code?: unknown }; if (typeof code !== 'string' || !/^\d{6}$/.test(code)) { res.status(400).json({ ok: false, reason: 'bad_code' }); return; } cleanPairCodes(); const entry = pairCodes.get(code); if (!entry) { res.status(401).json({ ok: false, reason: 'invalid_or_expired' }); return; } pairCodes.delete(code); setOwnerCookie(res); log.info({ ip }, 'Device paired via owner pair code'); res.json({ ok: true }); }); export default router;