From bf47d29ab5159ea1b2541797247d1572585c7967 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 23 Apr 2026 19:31:20 +0200 Subject: [PATCH] =?UTF-8?q?refactor(ts):=20day=203=20batch=201=20=E2=80=94?= =?UTF-8?q?=20convert=204=20small=20route=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates the 4 smallest and best-understood routes to TypeScript: src/routes/logs.ts src/routes/userPreferences.ts src/routes/sessions.ts src/routes/extensions.ts (11 Playwright tests cover this one) Pattern established for the remaining 25 routes: import express = require('express'); // CJS-style, fully typed import type { Request, Response } from 'express'; const db = require('../db/database'); // stays `any` until Day 4 const { authMiddleware } = require('../middleware/auth'); const router = express.Router(); router.get('/foo', async function (req: Request, res: Response) { // req.user is typed via src/types/express.d.ts augmentation }); export = router; // CJS-compatible export Why `import = require()` instead of `import from`: Express is imported via the namespace-import syntax so tsc preserves `require("express")` in the emitted CJS. Using plain ES import would emit `require("express").default` which doesn't exist on Express' CommonJS default export. The pattern keeps the compiled dist/*.js byte-identical to what vanilla Node expects. Why `const { authMiddleware } = require(...)` for other imports: The middleware + utils files are still .js and their module.exports shape isn't fully typed yet (Day 4 work). Using `require` avoids dragging Day 4 work forward; the destructuring still gives us the variable name we want. New file — src/types/express.d.ts: Augments Express.Request with `user?: AuthUser` and `sessionId?: string` (attached by authMiddleware). Route handlers now type-check against `req.user!.id` instead of requiring a runtime cast. `req.user!` uses non-null assertion because authMiddleware guarantees user is set for every mounted route; strict mode on Day 5 will keep the assertion but add a type-guard check inside authMiddleware itself so it propagates. Verification - npm run typecheck → 0 errors - scripts/lint-references.js → green - Compiled dist/src/routes/logs.js diffs against the original .js by whitespace + var→const only. Behavior preserved. Nothing deployed. Prod + e2e containers still run the previous image. Progress: 1/30 (server.ts) + 4/29 routes = 5 of 54 total files migrated. Remaining batches go in subsequent Day 3 commits. --- src/routes/{extensions.js => extensions.ts} | 113 +++++++++--------- src/routes/logs.js | 63 ---------- src/routes/logs.ts | 77 ++++++++++++ src/routes/{sessions.js => sessions.ts} | 31 ++--- ...{userPreferences.js => userPreferences.ts} | 69 +++++------ src/types/express.d.ts | 20 ++++ 6 files changed, 206 insertions(+), 167 deletions(-) rename src/routes/{extensions.js => extensions.ts} (61%) delete mode 100644 src/routes/logs.js create mode 100644 src/routes/logs.ts rename src/routes/{sessions.js => sessions.ts} (61%) rename src/routes/{userPreferences.js => userPreferences.ts} (79%) create mode 100644 src/types/express.d.ts diff --git a/src/routes/extensions.js b/src/routes/extensions.ts similarity index 61% rename from src/routes/extensions.js rename to src/routes/extensions.ts index 02e7616..d1a6769 100644 --- a/src/routes/extensions.js +++ b/src/routes/extensions.ts @@ -4,39 +4,42 @@ // Soft-delete via trashed_at: deleting moves to trash; user can restore // or purge. List supports async search (q) and trash toggle (trash=1). -var express = require('express'); -var router = express.Router(); -var db = require('../db/database'); -var { authMiddleware } = require('../middleware/auth'); -var logger = require('../utils/logger'); +import express = require('express'); +import type { Request, Response } from 'express'; +const db = require('../db/database'); +const { authMiddleware } = require('../middleware/auth'); +const logger = require('../utils/logger'); +const router = express.Router(); router.use(authMiddleware); -function sanitizeType(t) { - return (t === 'pager') ? 'pager' : 'extension'; +type ExtensionType = 'extension' | 'pager'; + +function sanitizeType(t: unknown): ExtensionType { + return t === 'pager' ? 'pager' : 'extension'; } -function clip(s, n) { +function clip(s: unknown, n?: number): string { return String(s == null ? '' : s).trim().substring(0, n || 200); } -function parseId(raw) { - var n = parseInt(raw, 10); - return (Number.isFinite(n) && n > 0) ? n : null; +function parseId(raw: unknown): number | null { + const n = parseInt(raw as string, 10); + return Number.isFinite(n) && n > 0 ? n : null; } // ── GET /api/extensions ───────────────────────────────────── // Query params: // trash=1 → return trashed items only (default: active only) // q=text → case-insensitive substring search on location/name/number/notes -router.get('/extensions', async function (req, res) { +router.get('/extensions', async function (req: Request, res: Response) { try { - var trashMode = req.query.trash === '1' || req.query.trash === 'true'; - var q = (req.query.q || '').trim(); + const trashMode = req.query.trash === '1' || req.query.trash === 'true'; + const q = ((req.query.q as string) || '').trim(); - var sql = + let sql = 'SELECT id, location, name, number, type, notes, trashed_at, created_at, updated_at ' + 'FROM user_phone_extensions WHERE user_id = $1 AND trashed_at IS ' + (trashMode ? 'NOT NULL' : 'NULL'); - var params = [req.user.id]; + const params: (string | number)[] = [req.user!.id]; if (q) { sql += ' AND (LOWER(location) LIKE $2 OR LOWER(name) LIKE $2 OR LOWER(number) LIKE $2 OR LOWER(notes) LIKE $2)'; @@ -44,110 +47,110 @@ router.get('/extensions', async function (req, res) { } sql += ' ORDER BY location ASC, type ASC, name ASC'; - var rows = await db.all(sql, params); + const rows = await db.all(sql, params); res.json({ success: true, items: rows }); - } catch (e) { + } catch (e: any) { logger.error('GET /extensions', e.message); res.status(500).json({ error: 'Request failed' }); } }); // ── POST /api/extensions ──────────────────────────────────── -router.post('/extensions', async function (req, res) { +router.post('/extensions', async function (req: Request, res: Response) { try { - var location = clip(req.body.location, 120); - var name = clip(req.body.name, 120); - var number = clip(req.body.number, 40); - var type = sanitizeType(req.body.type); - var notes = clip(req.body.notes, 500); + const location = clip(req.body.location, 120); + const name = clip(req.body.name, 120); + const number = clip(req.body.number, 40); + const type = sanitizeType(req.body.type); + const notes = clip(req.body.notes, 500); if (!location) return res.status(400).json({ error: 'Location is required' }); - if (!name) return res.status(400).json({ error: 'Name / department is required' }); - if (!number) return res.status(400).json({ error: 'Number is required' }); + if (!name) return res.status(400).json({ error: 'Name / department is required' }); + if (!number) return res.status(400).json({ error: 'Number is required' }); - var result = await db.run( + const result = await db.run( 'INSERT INTO user_phone_extensions (user_id, location, name, number, type, notes) VALUES ($1,$2,$3,$4,$5,$6)', - [req.user.id, location, name, number, type, notes] + [req.user!.id, location, name, number, type, notes] ); res.json({ success: true, id: result.lastInsertRowid }); - } catch (e) { + } catch (e: any) { logger.error('POST /extensions', e.message); res.status(500).json({ error: 'Request failed' }); } }); // ── PUT /api/extensions/:id ───────────────────────────────── -router.put('/extensions/:id', async function (req, res) { +router.put('/extensions/:id', async function (req: Request, res: Response) { try { - var id = parseId(req.params.id); + const id = parseId(req.params.id); if (!id) return res.status(400).json({ error: 'Invalid id' }); - var location = clip(req.body.location, 120); - var name = clip(req.body.name, 120); - var number = clip(req.body.number, 40); - var type = sanitizeType(req.body.type); - var notes = clip(req.body.notes, 500); + const location = clip(req.body.location, 120); + const name = clip(req.body.name, 120); + const number = clip(req.body.number, 40); + const type = sanitizeType(req.body.type); + const notes = clip(req.body.notes, 500); if (!location || !name || !number) return res.status(400).json({ error: 'Location, name, and number are required' }); - var result = await db.run( + const result = await db.run( 'UPDATE user_phone_extensions SET location=$1, name=$2, number=$3, type=$4, notes=$5, updated_at=NOW() WHERE id=$6 AND user_id=$7', - [location, name, number, type, notes, id, req.user.id] + [location, name, number, type, notes, id, req.user!.id] ); if (result.changes === 0) return res.status(404).json({ error: 'Not found' }); res.json({ success: true }); - } catch (e) { + } catch (e: any) { logger.error('PUT /extensions/:id', e.message); res.status(500).json({ error: 'Request failed' }); } }); // ── DELETE /api/extensions/:id — soft-delete (move to trash) ── -router.delete('/extensions/:id', async function (req, res) { +router.delete('/extensions/:id', async function (req: Request, res: Response) { try { - var id = parseId(req.params.id); + const id = parseId(req.params.id); if (!id) return res.status(400).json({ error: 'Invalid id' }); - var result = await db.run( + const result = await db.run( 'UPDATE user_phone_extensions SET trashed_at = NOW(), updated_at = NOW() WHERE id=$1 AND user_id=$2 AND trashed_at IS NULL', - [id, req.user.id] + [id, req.user!.id] ); if (result.changes === 0) return res.status(404).json({ error: 'Not found or already trashed' }); res.json({ success: true }); - } catch (e) { + } catch (e: any) { logger.error('DELETE /extensions/:id', e.message); res.status(500).json({ error: 'Request failed' }); } }); // ── POST /api/extensions/:id/restore — bring back from trash ── -router.post('/extensions/:id/restore', async function (req, res) { +router.post('/extensions/:id/restore', async function (req: Request, res: Response) { try { - var id = parseId(req.params.id); + const id = parseId(req.params.id); if (!id) return res.status(400).json({ error: 'Invalid id' }); - var result = await db.run( + const result = await db.run( 'UPDATE user_phone_extensions SET trashed_at = NULL, updated_at = NOW() WHERE id=$1 AND user_id=$2 AND trashed_at IS NOT NULL', - [id, req.user.id] + [id, req.user!.id] ); if (result.changes === 0) return res.status(404).json({ error: 'Not found in trash' }); res.json({ success: true }); - } catch (e) { + } catch (e: any) { logger.error('POST /extensions/:id/restore', e.message); res.status(500).json({ error: 'Request failed' }); } }); // ── DELETE /api/extensions/:id/purge — hard delete (only from trash) ── -router.delete('/extensions/:id/purge', async function (req, res) { +router.delete('/extensions/:id/purge', async function (req: Request, res: Response) { try { - var id = parseId(req.params.id); + const id = parseId(req.params.id); if (!id) return res.status(400).json({ error: 'Invalid id' }); - var result = await db.run( + const result = await db.run( 'DELETE FROM user_phone_extensions WHERE id=$1 AND user_id=$2 AND trashed_at IS NOT NULL', - [id, req.user.id] + [id, req.user!.id] ); if (result.changes === 0) return res.status(404).json({ error: 'Must trash before purge' }); res.json({ success: true }); - } catch (e) { + } catch (e: any) { logger.error('DELETE /extensions/:id/purge', e.message); res.status(500).json({ error: 'Request failed' }); } }); -module.exports = router; +export = router; diff --git a/src/routes/logs.js b/src/routes/logs.js deleted file mode 100644 index e8cd7ee..0000000 --- a/src/routes/logs.js +++ /dev/null @@ -1,63 +0,0 @@ -var express = require('express'); -var router = express.Router(); -var { authMiddleware } = require('../middleware/auth'); -var logger = require('../utils/logger'); - -router.get('/logs/usage', authMiddleware, async function(req, res) { - try { - var days = parseInt(req.query.days) || 30; - var summary = await logger.getUsageSummary(req.user.id, days); - res.json({ success: true, summary: summary, period: days + ' days' }); - } catch (err) { res.status(500).json({ error: 'Request failed' }); } -}); - -router.get('/logs/audit', authMiddleware, async function(req, res) { - try { - var logs = await logger.getAuditLogs(req.user.id, parseInt(req.query.limit) || 50); - res.json({ success: true, logs: logs }); - } catch (err) { res.status(500).json({ error: 'Request failed' }); } -}); - -router.get('/logs/api', authMiddleware, async function(req, res) { - try { - var logs = await logger.getApiLogs(req.user.id, parseInt(req.query.limit) || 50); - res.json({ success: true, logs: logs }); - } catch (err) { res.status(500).json({ error: 'Request failed' }); } -}); - -router.get('/logs/access', authMiddleware, async function(req, res) { - try { - var logs = await logger.getAccessLogs(req.user.id, parseInt(req.query.limit) || 50); - res.json({ success: true, logs: logs }); - } catch (err) { res.status(500).json({ error: 'Request failed' }); } -}); - -// Client-side error receiver — no auth required (sendBeacon can't send auth headers) -router.post('/logs/client-error', function(req, res) { - try { - var e = req.body || {}; - console.error('[CLIENT ERROR]', JSON.stringify({ - type: e.type, - message: e.message, - source: e.source, - line: e.line, - col: e.col, - stack: e.stack, - ip: req.ip, - ua: req.headers['user-agent'], - time: new Date().toISOString() - })); - res.status(204).end(); - } catch(err) { res.status(204).end(); } -}); - -// Client-side event logging (copy, export — PHI access tracking) -router.post('/logs/client-event', authMiddleware, function(req, res) { - try { - var e = req.body || {}; - logger.audit(req.user.id, e.action || 'client_event', e.target || '', req, { category: 'phi_access' }); - res.status(204).end(); - } catch(err) { res.status(204).end(); } -}); - -module.exports = router; diff --git a/src/routes/logs.ts b/src/routes/logs.ts new file mode 100644 index 0000000..b900c0b --- /dev/null +++ b/src/routes/logs.ts @@ -0,0 +1,77 @@ +import express = require('express'); +import type { Request, Response } from 'express'; +const { authMiddleware } = require('../middleware/auth'); +const logger = require('../utils/logger'); + +const router = express.Router(); + +router.get('/logs/usage', authMiddleware, async function (req: Request, res: Response) { + try { + const days = parseInt(req.query.days as string) || 30; + const summary = await logger.getUsageSummary(req.user!.id, days); + res.json({ success: true, summary, period: days + ' days' }); + } catch (err) { + res.status(500).json({ error: 'Request failed' }); + } +}); + +router.get('/logs/audit', authMiddleware, async function (req: Request, res: Response) { + try { + const logs = await logger.getAuditLogs(req.user!.id, parseInt(req.query.limit as string) || 50); + res.json({ success: true, logs }); + } catch (err) { + res.status(500).json({ error: 'Request failed' }); + } +}); + +router.get('/logs/api', authMiddleware, async function (req: Request, res: Response) { + try { + const logs = await logger.getApiLogs(req.user!.id, parseInt(req.query.limit as string) || 50); + res.json({ success: true, logs }); + } catch (err) { + res.status(500).json({ error: 'Request failed' }); + } +}); + +router.get('/logs/access', authMiddleware, async function (req: Request, res: Response) { + try { + const logs = await logger.getAccessLogs(req.user!.id, parseInt(req.query.limit as string) || 50); + res.json({ success: true, logs }); + } catch (err) { + res.status(500).json({ error: 'Request failed' }); + } +}); + +// Client-side error receiver — no auth required (sendBeacon can't send auth headers) +router.post('/logs/client-error', function (req: Request, res: Response) { + try { + const e = req.body || {}; + console.error('[CLIENT ERROR]', JSON.stringify({ + type: e.type, + message: e.message, + source: e.source, + line: e.line, + col: e.col, + stack: e.stack, + ip: req.ip, + ua: req.headers['user-agent'], + time: new Date().toISOString(), + })); + res.status(204).end(); + } catch (err) { + res.status(204).end(); + } +}); + +// Client-side event logging (copy, export — PHI access tracking) +router.post('/logs/client-event', authMiddleware, function (req: Request, res: Response) { + try { + const e = req.body || {}; + logger.audit(req.user!.id, e.action || 'client_event', e.target || '', req, { category: 'phi_access' }); + res.status(204).end(); + } catch (err) { + res.status(204).end(); + } +}); + +export = router; diff --git a/src/routes/sessions.js b/src/routes/sessions.ts similarity index 61% rename from src/routes/sessions.js rename to src/routes/sessions.ts index 0488a62..03b1909 100644 --- a/src/routes/sessions.js +++ b/src/routes/sessions.ts @@ -1,32 +1,33 @@ -var express = require('express'); -var router = express.Router(); -var db = require('../db/database'); -var { authMiddleware } = require('../middleware/auth'); +import express = require('express'); +import type { Request, Response } from 'express'; +const db = require('../db/database'); +const { authMiddleware } = require('../middleware/auth'); +const router = express.Router(); router.use(authMiddleware); // List all sessions for current user -router.get('/', async function(req, res) { +router.get('/', async function (req: Request, res: Response) { try { - var sessions = await db.all( + const sessions = await db.all( 'SELECT id, ip_address, device_label, created_at, last_activity FROM user_sessions WHERE user_id = ? ORDER BY last_activity DESC', - [req.user.id] + [req.user!.id] ); - res.json({ success: true, sessions: sessions, currentSessionId: req.sessionId || null }); + res.json({ success: true, sessions, currentSessionId: req.sessionId || null }); } catch (e) { res.status(500).json({ error: 'Request failed' }); } }); // Revoke a specific session -router.delete('/:id', async function(req, res) { +router.delete('/:id', async function (req: Request, res: Response) { try { if (req.params.id === req.sessionId) { return res.status(400).json({ error: 'Cannot revoke your current session. Use logout instead.' }); } - var result = await db.run( + const result = await db.run( 'DELETE FROM user_sessions WHERE id = ? AND user_id = ?', - [req.params.id, req.user.id] + [req.params.id, req.user!.id] ); if (result.changes === 0) return res.status(404).json({ error: 'Session not found' }); res.json({ success: true }); @@ -36,12 +37,12 @@ router.delete('/:id', async function(req, res) { }); // Revoke all other sessions -router.delete('/', async function(req, res) { +router.delete('/', async function (req: Request, res: Response) { try { if (!req.sessionId) return res.status(400).json({ error: 'Current session not identified. Log out and log back in.' }); - var result = await db.run( + const result = await db.run( 'DELETE FROM user_sessions WHERE user_id = ? AND id != ?', - [req.user.id, req.sessionId] + [req.user!.id, req.sessionId] ); res.json({ success: true, revoked: result.changes }); } catch (e) { @@ -49,4 +50,4 @@ router.delete('/', async function(req, res) { } }); -module.exports = router; +export = router; diff --git a/src/routes/userPreferences.js b/src/routes/userPreferences.ts similarity index 79% rename from src/routes/userPreferences.js rename to src/routes/userPreferences.ts index 1556d9f..3a495f6 100644 --- a/src/routes/userPreferences.js +++ b/src/routes/userPreferences.ts @@ -2,50 +2,54 @@ // USER PREFERENCES — STT model & TTS voice selection // ============================================================ -var express = require('express'); -var router = express.Router(); -var db = require('../db/database'); -var { authMiddleware } = require('../middleware/auth'); +import express = require('express'); +import type { Request, Response } from 'express'; +const db = require('../db/database'); +const { authMiddleware } = require('../middleware/auth'); +const router = express.Router(); router.use(authMiddleware); +interface SttTtsOption { + value: string; + label: string; +} + // Get current user's STT/TTS preferences -router.get('/preferences', async function(req, res) { +router.get('/preferences', async function (req: Request, res: Response) { try { - var user = await db.get('SELECT stt_model, tts_voice FROM users WHERE id = ?', [req.user.id]); + const user = await db.get('SELECT stt_model, tts_voice FROM users WHERE id = ?', [req.user!.id]); res.json({ success: true, stt_model: user?.stt_model || null, - tts_voice: user?.tts_voice || null + tts_voice: user?.tts_voice || null, }); - } catch (err) { + } catch (err: any) { console.error('[Preferences]', err.message); res.status(500).json({ error: 'Request failed' }); } }); // Update user's STT/TTS preferences -router.post('/preferences', async function(req, res) { +router.post('/preferences', async function (req: Request, res: Response) { try { - var { stt_model, tts_voice } = req.body; - + const { stt_model, tts_voice } = req.body; await db.run( 'UPDATE users SET stt_model = ?, tts_voice = ? WHERE id = ?', - [stt_model || null, tts_voice || null, req.user.id] + [stt_model || null, tts_voice || null, req.user!.id] ); - res.json({ success: true }); - } catch (err) { + } catch (err: any) { console.error('[Preferences]', err.message); res.status(500).json({ error: 'Request failed' }); } }); // Get available STT models and TTS voices -router.get('/preferences/options', async function(req, res) { +router.get('/preferences/options', async function (_req: Request, res: Response) { try { - var provider = process.env.TRANSCRIBE_PROVIDER; - var ttsProvider = process.env.TTS_PROVIDER; + let provider = process.env.TRANSCRIBE_PROVIDER; + let ttsProvider = process.env.TTS_PROVIDER; // Auto-detect providers if not explicitly set if (!provider) { @@ -63,13 +67,13 @@ router.get('/preferences/options', async function(req, res) { } // STT Models - var sttModels = []; + let sttModels: SttTtsOption[] = []; if (provider === 'google') { sttModels = [ { value: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash (Experimental, fastest)' }, { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash (Fast, accurate)' }, { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash (Stable)' }, - { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro (Best quality, slower)' } + { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro (Best quality, slower)' }, ]; } else if (provider === 'litellm') { sttModels = [ @@ -78,16 +82,14 @@ router.get('/preferences/options', async function(req, res) { { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' }, { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' }, { value: 'whisper-1', label: 'OpenAI Whisper-1' }, - { value: 'whisper-large-v3', label: 'Whisper Large v3' } + { value: 'whisper-large-v3', label: 'Whisper Large v3' }, ]; } else if (provider === 'openai') { - sttModels = [ - { value: 'whisper-1', label: 'Whisper-1 (OpenAI standard)' } - ]; + sttModels = [{ value: 'whisper-1', label: 'Whisper-1 (OpenAI standard)' }]; } else if (provider === 'aws') { sttModels = [ { value: 'default', label: 'AWS Transcribe (Standard)' }, - { value: 'medical', label: 'AWS Transcribe Medical' } + { value: 'medical', label: 'AWS Transcribe Medical' }, ]; } else if (provider === 'local') { sttModels = [ @@ -95,12 +97,12 @@ router.get('/preferences/options', async function(req, res) { { value: 'base', label: 'Whisper Base' }, { value: 'small', label: 'Whisper Small' }, { value: 'medium', label: 'Whisper Medium' }, - { value: 'large', label: 'Whisper Large (best quality)' } + { value: 'large', label: 'Whisper Large (best quality)' }, ]; } // TTS Voices - var ttsVoices = []; + let ttsVoices: SttTtsOption[] = []; if (ttsProvider === 'google') { ttsVoices = [ { value: 'en-US-Journey-F', label: 'Journey (Female, natural)' }, @@ -115,7 +117,7 @@ router.get('/preferences/options', async function(req, res) { { value: 'en-US-Neural2-G', label: 'Neural2 G (Female, standard)' }, { value: 'en-US-Neural2-H', label: 'Neural2 H (Female, standard)' }, { value: 'en-US-Neural2-I', label: 'Neural2 I (Male, standard)' }, - { value: 'en-US-Neural2-J', label: 'Neural2 J (Male, standard)' } + { value: 'en-US-Neural2-J', label: 'Neural2 J (Male, standard)' }, ]; } else if (ttsProvider === 'litellm' || ttsProvider === 'openai') { ttsVoices = [ @@ -124,7 +126,7 @@ router.get('/preferences/options', async function(req, res) { { value: 'fable', label: 'Fable (British Male)' }, { value: 'onyx', label: 'Onyx (Deep Male)' }, { value: 'nova', label: 'Nova (Female)' }, - { value: 'shimmer', label: 'Shimmer (Soft Female)' } + { value: 'shimmer', label: 'Shimmer (Soft Female)' }, ]; } else if (ttsProvider === 'elevenlabs') { ttsVoices = [ @@ -136,22 +138,21 @@ router.get('/preferences/options', async function(req, res) { { value: 'elli', label: 'Elli (Female, young)' }, { value: 'josh', label: 'Josh (Male, narration)' }, { value: 'arnold', label: 'Arnold (Male, crisp)' }, - { value: 'sam', label: 'Sam (Male, raspy)' } + { value: 'sam', label: 'Sam (Male, raspy)' }, ]; } res.json({ success: true, sttProvider: provider || 'none', - sttModels: sttModels, + sttModels, ttsProvider: ttsProvider || 'browser', - ttsVoices: ttsVoices + ttsVoices, }); - - } catch (err) { + } catch (err: any) { console.error('[Preferences]', err.message); res.status(500).json({ error: 'Request failed' }); } }); -module.exports = router; +export = router; diff --git a/src/types/express.d.ts b/src/types/express.d.ts new file mode 100644 index 0000000..0e5b55c --- /dev/null +++ b/src/types/express.d.ts @@ -0,0 +1,20 @@ +// ============================================================ +// EXPRESS REQUEST AUGMENTATION +// ============================================================ +// authMiddleware attaches `user` and `sessionId` to every request +// after token verification. Declaring them here so every route +// handler sees them typed without casting. +// ============================================================ + +import type { AuthUser } from '../../shared/types'; + +declare global { + namespace Express { + interface Request { + user?: AuthUser; + sessionId?: string; + } + } +} + +export {};