From 9f8d3f8d99fd6d17c6d7676f6d949f91c2c8e94d Mon Sep 17 00:00:00 2001 From: Stephen Wheet Date: Sat, 28 Mar 2026 19:33:18 +0000 Subject: [PATCH 1/5] feat: add OIDC-only mode to disable password authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When OIDC is configured, admins can now enable 'Disable password authentication' in Admin → Settings → SSO. This blocks all password- based login and registration, forcing users through the SSO identity provider instead. Backend: - routes/admin.ts: expose oidc_only flag on GET /admin/oidc and accept it on PUT /admin/oidc (persisted to app_settings) - routes/auth.ts: add isOidcOnlyMode() helper; block POST /auth/login, POST /auth/register (for non-first-user), and PUT /auth/me/password with HTTP 403 when OIDC-only mode is active - routes/auth.ts: expose oidc_only_mode boolean in GET /auth/app-config Frontend: - AdminPage: toggle in OIDC/SSO settings section (oidc_only saved with rest of OIDC config on same Save button) - LoginPage: when oidc_only_mode is active, replace form with a single-button OIDC redirect; hide register toggle - SettingsPage: hide password change section when oidc_only_mode is on - i18n (en/de): admin.oidcOnlyMode, admin.oidcOnlyModeHint, login.oidcOnly --- client/src/i18n/translations/de.ts | 3 +++ client/src/i18n/translations/en.ts | 3 +++ client/src/pages/AdminPage.tsx | 25 +++++++++++++++++-- client/src/pages/LoginPage.tsx | 39 +++++++++++++++++++++++++++--- client/src/pages/SettingsPage.tsx | 9 +++++++ server/src/routes/admin.ts | 4 ++- server/src/routes/auth.ts | 24 ++++++++++++++++++ 7 files changed, 101 insertions(+), 6 deletions(-) diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 2bec291..0421e1f 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -206,6 +206,7 @@ const de: Record = { 'login.oidc.invalidState': 'Ungültige Sitzung. Bitte erneut versuchen.', 'login.demoFailed': 'Demo-Login fehlgeschlagen', 'login.oidcSignIn': 'Anmelden mit {name}', + 'login.oidcOnly': 'Passwort-Authentifizierung ist deaktiviert. Bitte melde dich über deinen SSO-Anbieter an.', 'login.demoHint': 'Demo ausprobieren — ohne Registrierung', // Register @@ -285,6 +286,8 @@ const de: Record = { 'admin.oidcIssuer': 'Issuer URL', 'admin.oidcIssuerHint': 'Die OpenID Connect Issuer URL des Anbieters. z.B. https://accounts.google.com', 'admin.oidcSaved': 'OIDC-Konfiguration gespeichert', + 'admin.oidcOnlyMode': 'Passwort-Authentifizierung deaktivieren', + 'admin.oidcOnlyModeHint': 'Wenn aktiviert, ist nur SSO-Login erlaubt. Passwort-Login und Registrierung werden blockiert.', // File Types 'admin.fileTypes': 'Erlaubte Dateitypen', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 73c42a2..33c828a 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -206,6 +206,7 @@ const en: Record = { 'login.oidc.invalidState': 'Invalid session. Please try again.', 'login.demoFailed': 'Demo login failed', 'login.oidcSignIn': 'Sign in with {name}', + 'login.oidcOnly': 'Password authentication is disabled. Please sign in using your SSO provider.', 'login.demoHint': 'Try the demo — no registration needed', // Register @@ -285,6 +286,8 @@ const en: Record = { 'admin.oidcIssuer': 'Issuer URL', 'admin.oidcIssuerHint': 'The OpenID Connect Issuer URL of the provider. e.g. https://accounts.google.com', 'admin.oidcSaved': 'OIDC configuration saved', + 'admin.oidcOnlyMode': 'Disable password authentication', + 'admin.oidcOnlyModeHint': 'When enabled, only SSO login is permitted. Password-based login and registration are blocked.', // File Types 'admin.fileTypes': 'Allowed File Types', diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index 8f72c83..23369ef 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -39,6 +39,7 @@ interface OidcConfig { client_secret: string client_secret_set: boolean display_name: string + oidc_only: boolean } interface UpdateInfo { @@ -72,7 +73,7 @@ export default function AdminPage(): React.ReactElement { const [createForm, setCreateForm] = useState<{ username: string; email: string; password: string; role: string }>({ username: '', email: '', password: '', role: 'user' }) // OIDC config - const [oidcConfig, setOidcConfig] = useState({ issuer: '', client_id: '', client_secret: '', client_secret_set: false, display_name: '' }) + const [oidcConfig, setOidcConfig] = useState({ issuer: '', client_id: '', client_secret: '', client_secret_set: false, display_name: '', oidc_only: false }) const [savingOidc, setSavingOidc] = useState(false) // Registration toggle @@ -715,11 +716,31 @@ export default function AdminPage(): React.ReactElement { className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" /> + {/* OIDC-only mode toggle */} +
+
+

{t('admin.oidcOnlyMode')}

+

{t('admin.oidcOnlyModeHint')}

+
+ +
+

)} + )} - {/* OIDC / SSO login button */} - {appConfig?.oidc_configured && ( + {/* OIDC / SSO login button (only when OIDC is configured but not in oidc-only mode) */} + {appConfig?.oidc_configured && !oidcOnly && ( <>
diff --git a/client/src/pages/SettingsPage.tsx b/client/src/pages/SettingsPage.tsx index bf19952..1d13881 100644 --- a/client/src/pages/SettingsPage.tsx +++ b/client/src/pages/SettingsPage.tsx @@ -71,6 +71,13 @@ export default function SettingsPage(): React.ReactElement { const [currentPassword, setCurrentPassword] = useState('') const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') + const [oidcOnlyMode, setOidcOnlyMode] = useState(false) + + useEffect(() => { + authApi.getAppConfig?.().then((config) => { + if (config?.oidc_only_mode) setOidcOnlyMode(true) + }).catch(() => {}) + }, []) useEffect(() => { setMapTileUrl(settings.map_tile_url || '') @@ -398,6 +405,7 @@ export default function SettingsPage(): React.ReactElement {
{/* Change Password */} + {!oidcOnlyMode && (
@@ -446,6 +454,7 @@ export default function SettingsPage(): React.ReactElement {
+ )}
diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index 53e538b..f6d0afd 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -122,16 +122,18 @@ router.get('/oidc', (_req: Request, res: Response) => { client_id: get('oidc_client_id'), client_secret_set: !!secret, display_name: get('oidc_display_name'), + oidc_only: get('oidc_only') === 'true', }); }); router.put('/oidc', (req: Request, res: Response) => { - const { issuer, client_id, client_secret, display_name } = req.body; + const { issuer, client_id, client_secret, display_name, oidc_only } = req.body; const set = (key: string, val: string) => db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)").run(key, val || ''); set('oidc_issuer', issuer); set('oidc_client_id', client_id); if (client_secret !== undefined) set('oidc_client_secret', client_secret); set('oidc_display_name', display_name); + set('oidc_only', oidc_only ? 'true' : 'false'); res.json({ success: true }); }); diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 2996128..7516d3a 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -59,6 +59,17 @@ function rateLimiter(maxAttempts: number, windowMs: number) { } const authLimiter = rateLimiter(10, RATE_LIMIT_WINDOW); +function isOidcOnlyMode(): boolean { + const get = (key: string) => (db.prepare("SELECT value FROM app_settings WHERE key = ?").get(key) as { value: string } | undefined)?.value || null; + const enabled = get('oidc_only') === 'true'; + if (!enabled) return false; + const oidcConfigured = !!( + (process.env.OIDC_ISSUER || get('oidc_issuer')) && + (process.env.OIDC_CLIENT_ID || get('oidc_client_id')) + ); + return oidcConfigured; +} + function maskKey(key: string | null | undefined): string | null { if (!key) return null; if (key.length <= 8) return '--------'; @@ -89,6 +100,8 @@ router.get('/app-config', (_req: Request, res: Response) => { (process.env.OIDC_ISSUER || (db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_issuer'").get() as { value: string } | undefined)?.value) && (process.env.OIDC_CLIENT_ID || (db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_client_id'").get() as { value: string } | undefined)?.value) ); + const oidcOnlySetting = (db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_only'").get() as { value: string } | undefined)?.value; + const oidcOnlyMode = oidcConfigured && oidcOnlySetting === 'true'; res.json({ allow_registration: isDemo ? false : allowRegistration, has_users: userCount > 0, @@ -96,6 +109,7 @@ router.get('/app-config', (_req: Request, res: Response) => { has_maps_key: hasGoogleKey, oidc_configured: oidcConfigured, oidc_display_name: oidcConfigured ? (oidcDisplayName || 'SSO') : undefined, + oidc_only_mode: oidcOnlyMode, allowed_file_types: (db.prepare("SELECT value FROM app_settings WHERE key = 'allowed_file_types'").get() as { value: string } | undefined)?.value || 'jpg,jpeg,png,gif,webp,heic,pdf,doc,docx,xls,xlsx,txt,csv', demo_mode: isDemo, demo_email: isDemo ? 'demo@trek.app' : undefined, @@ -118,6 +132,9 @@ router.post('/register', authLimiter, (req: Request, res: Response) => { const { username, email, password } = req.body; const userCount = (db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }).count; + if (userCount > 0 && isOidcOnlyMode()) { + return res.status(403).json({ error: 'Password authentication is disabled. Please sign in with SSO.' }); + } if (userCount > 0) { const setting = db.prepare("SELECT value FROM app_settings WHERE key = 'allow_registration'").get() as { value: string } | undefined; if (setting?.value === 'false') { @@ -167,6 +184,10 @@ router.post('/register', authLimiter, (req: Request, res: Response) => { }); router.post('/login', authLimiter, (req: Request, res: Response) => { + if (isOidcOnlyMode()) { + return res.status(403).json({ error: 'Password authentication is disabled. Please sign in with SSO.' }); + } + const { email, password } = req.body; if (!email || !password) { @@ -205,6 +226,9 @@ router.get('/me', authenticate, (req: Request, res: Response) => { router.put('/me/password', authenticate, rateLimiter(5, RATE_LIMIT_WINDOW), (req: Request, res: Response) => { const authReq = req as AuthRequest; + if (isOidcOnlyMode()) { + return res.status(403).json({ error: 'Password authentication is disabled.' }); + } if (process.env.DEMO_MODE === 'true' && authReq.user.email === 'demo@trek.app') { return res.status(403).json({ error: 'Password change is disabled in demo mode.' }); } From 77f2c616dee7b17c217ca561dc0b9f3c1ca36f5a Mon Sep 17 00:00:00 2001 From: Stephen Wheet Date: Sat, 28 Mar 2026 19:41:06 +0000 Subject: [PATCH 2/5] fix: type error in AdminPage handleSaveUser payload, install deps --- client/package-lock.json | 4 ++-- client/src/pages/AdminPage.tsx | 2 +- server/package-lock.json | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 05a5fdf..9a80671 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "trek-client", - "version": "2.6.1", + "version": "2.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trek-client", - "version": "2.6.1", + "version": "2.6.2", "dependencies": { "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index 23369ef..44a6ce9 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -247,7 +247,7 @@ export default function AdminPage(): React.ReactElement { const handleSaveUser = async () => { try { - const payload = { + const payload: { username?: string; email?: string; role: string; password?: string } = { username: editForm.username.trim() || undefined, email: editForm.email.trim() || undefined, role: editForm.role, diff --git a/server/package-lock.json b/server/package-lock.json index 28bc934..77c8247 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { - "name": "nomad-server", - "version": "2.6.1", + "name": "trek-server", + "version": "2.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "nomad-server", - "version": "2.6.1", + "name": "trek-server", + "version": "2.6.2", "dependencies": { "archiver": "^6.0.1", "bcryptjs": "^2.4.3", From fcbfeb679300863cbd4a44073453c6efc6c73b65 Mon Sep 17 00:00:00 2001 From: Stephen Wheet Date: Sat, 28 Mar 2026 19:45:01 +0000 Subject: [PATCH 3/5] fix: resolve all TypeScript errors - node types, Express v4 types, places/scheduler fixes --- server/package-lock.json | 44 +++++++++++++++++++++++++++---------- server/package.json | 2 +- server/src/routes/admin.ts | 2 +- server/src/routes/places.ts | 6 ++--- server/src/scheduler.ts | 6 ++--- server/tsconfig.json | 1 + 6 files changed, 41 insertions(+), 20 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 77c8247..4f8b27d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -30,7 +30,7 @@ "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", + "@types/express": "^4.17.25", "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.1.0", "@types/node": "^25.5.0", @@ -516,21 +516,22 @@ } }, "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "dev": true, "license": "MIT", "dependencies": { @@ -558,6 +559,13 @@ "@types/node": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -627,13 +635,25 @@ } }, "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", "@types/node": "*" } }, diff --git a/server/package.json b/server/package.json index 39ae754..becc76e 100644 --- a/server/package.json +++ b/server/package.json @@ -29,7 +29,7 @@ "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", + "@types/express": "^4.17.25", "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.1.0", "@types/node": "^25.5.0", diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index f6d0afd..b460e77 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -94,7 +94,7 @@ router.put('/users/:id', (req: Request, res: Response) => { router.delete('/users/:id', (req: Request, res: Response) => { const authReq = req as AuthRequest; - if (parseInt(req.params.id) === authReq.user.id) { + if (parseInt(req.params.id as string) === authReq.user.id) { return res.status(400).json({ error: 'Cannot delete own account' }); } diff --git a/server/src/routes/places.ts b/server/src/routes/places.ts index 9acddb7..8dae83d 100644 --- a/server/src/routes/places.ts +++ b/server/src/routes/places.ts @@ -41,12 +41,12 @@ router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) = if (category) { query += ' AND p.category_id = ?'; - params.push(category); + params.push(category as string); } if (tag) { query += ' AND p.id IN (SELECT place_id FROM place_tags WHERE tag_id = ?)'; - params.push(tag); + params.push(tag as string); } query += ' ORDER BY p.created_at DESC'; @@ -107,7 +107,7 @@ router.post('/', authenticate, requireTripAccess, validateStringLengths({ name: } } - const place = getPlaceWithTags(placeId); + const place = getPlaceWithTags(Number(placeId)); res.status(201).json({ place }); broadcast(tripId, 'place:created', { place }, req.headers['x-socket-id'] as string); }); diff --git a/server/src/scheduler.ts b/server/src/scheduler.ts index 0d415f3..2a8a75d 100644 --- a/server/src/scheduler.ts +++ b/server/src/scheduler.ts @@ -1,4 +1,4 @@ -import cron from 'node-cron'; +import cron, { type ScheduledTask } from 'node-cron'; import archiver from 'archiver'; import path from 'path'; import fs from 'fs'; @@ -23,7 +23,7 @@ interface BackupSettings { keep_days: number; } -let currentTask: cron.ScheduledTask | null = null; +let currentTask: ScheduledTask | null = null; function loadSettings(): BackupSettings { try { @@ -110,7 +110,7 @@ function start(): void { } // Demo mode: hourly reset of demo user data -let demoTask: cron.ScheduledTask | null = null; +let demoTask: ScheduledTask | null = null; function startDemoReset(): void { if (demoTask) { demoTask.stop(); demoTask = null; } diff --git a/server/tsconfig.json b/server/tsconfig.json index 0ecff37..4659864 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -9,6 +9,7 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, + "types": ["node"], "resolveJsonModule": true, "declaration": false, "sourceMap": true, From d50c84b75505f80ac8bd787d659d7a87ef8544ad Mon Sep 17 00:00:00 2001 From: Stephen Wheet Date: Sat, 28 Mar 2026 20:13:24 +0000 Subject: [PATCH 4/5] fix: resolve all TypeScript errors via proper Express 5 typed route params - Add StringParams = Record to types.ts - Use Request in all route handlers across 14 files - Clean up earlier as-cast workarounds in places.ts and admin.ts - tsconfig.json: keep original (removed bad 'types:node' addition) - package.json: restore @types/express back to ^5.0.6 --- server/package-lock.json | 44 +++++++++---------------------- server/package.json | 2 +- server/src/index.ts | 3 ++- server/src/routes/assignments.ts | 18 ++++++------- server/src/routes/atlas.ts | 6 ++--- server/src/routes/backup.ts | 13 ++++----- server/src/routes/budget.ts | 16 +++++------ server/src/routes/collab.ts | 34 ++++++++++++------------ server/src/routes/dayNotes.ts | 10 +++---- server/src/routes/days.ts | 18 ++++++------- server/src/routes/files.ts | 18 ++++++------- server/src/routes/maps.ts | 10 +++---- server/src/routes/packing.ts | 12 ++++----- server/src/routes/places.ts | 26 +++++++++--------- server/src/routes/reservations.ts | 10 +++---- server/src/routes/trips.ts | 20 +++++++------- server/src/routes/vacay.ts | 38 +++++++++++++------------- server/src/types.ts | 4 +++ server/tsconfig.json | 1 - 19 files changed, 144 insertions(+), 159 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 4f8b27d..77c8247 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -30,7 +30,7 @@ "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", - "@types/express": "^4.17.25", + "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.1.0", "@types/node": "^25.5.0", @@ -516,22 +516,21 @@ } }, "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "dev": true, "license": "MIT", "dependencies": { @@ -559,13 +558,6 @@ "@types/node": "*" } }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -635,25 +627,13 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, diff --git a/server/package.json b/server/package.json index becc76e..39ae754 100644 --- a/server/package.json +++ b/server/package.json @@ -29,7 +29,7 @@ "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", - "@types/express": "^4.17.25", + "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.1.0", "@types/node": "^25.5.0", diff --git a/server/src/index.ts b/server/src/index.ts index 7b1b09a..203ec47 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,5 +1,6 @@ import 'dotenv/config'; import express, { Request, Response, NextFunction } from 'express'; +import { StringParams } from './types'; import cors from 'cors'; import helmet from 'helmet'; import path from 'path'; @@ -78,7 +79,7 @@ app.use(express.urlencoded({ extended: true })); app.use('/uploads/avatars', express.static(path.join(__dirname, '../uploads/avatars'))); // All other uploads require authentication -app.get('/uploads/:type/:filename', (req: Request, res: Response) => { +app.get('/uploads/:type/:filename', (req: Request, res: Response) => { const { type, filename } = req.params; const allowedTypes = ['covers', 'files', 'photos']; if (!allowedTypes.includes(type)) return res.status(404).send('Not found'); diff --git a/server/src/routes/assignments.ts b/server/src/routes/assignments.ts index cb2d9a9..f84e269 100644 --- a/server/src/routes/assignments.ts +++ b/server/src/routes/assignments.ts @@ -4,7 +4,7 @@ import { authenticate } from '../middleware/auth'; import { requireTripAccess } from '../middleware/tripAccess'; import { broadcast } from '../websocket'; import { loadTagsByPlaceIds, loadParticipantsByAssignmentIds, formatAssignmentWithPlace } from '../services/queryHelpers'; -import { AuthRequest, AssignmentRow, DayAssignment, Tag, Participant } from '../types'; +import { StringParams, AuthRequest, AssignmentRow, DayAssignment, Tag, Participant } from '../types'; const router = express.Router({ mergeParams: true }); @@ -75,7 +75,7 @@ function getAssignmentWithPlace(assignmentId: number | bigint) { }; } -router.get('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.get('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, dayId } = req.params; const day = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(dayId, tripId); @@ -109,7 +109,7 @@ router.get('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripAc res.json({ assignments: result }); }); -router.post('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.post('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, dayId } = req.params; const { place_id, notes } = req.body; @@ -131,7 +131,7 @@ router.post('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripA broadcast(tripId, 'assignment:created', { assignment }, req.headers['x-socket-id'] as string); }); -router.delete('/trips/:tripId/days/:dayId/assignments/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.delete('/trips/:tripId/days/:dayId/assignments/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, dayId, id } = req.params; const assignment = db.prepare( @@ -145,7 +145,7 @@ router.delete('/trips/:tripId/days/:dayId/assignments/:id', authenticate, requir broadcast(tripId, 'assignment:deleted', { assignmentId: Number(id), dayId: Number(dayId) }, req.headers['x-socket-id'] as string); }); -router.put('/trips/:tripId/days/:dayId/assignments/reorder', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.put('/trips/:tripId/days/:dayId/assignments/reorder', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, dayId } = req.params; const { orderedIds } = req.body; @@ -167,7 +167,7 @@ router.put('/trips/:tripId/days/:dayId/assignments/reorder', authenticate, requi broadcast(tripId, 'assignment:reordered', { dayId: Number(dayId), orderedIds }, req.headers['x-socket-id'] as string); }); -router.put('/trips/:tripId/assignments/:id/move', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.put('/trips/:tripId/assignments/:id/move', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const { new_day_id, order_index } = req.body; @@ -190,7 +190,7 @@ router.put('/trips/:tripId/assignments/:id/move', authenticate, requireTripAcces broadcast(tripId, 'assignment:moved', { assignment: updated, oldDayId: Number(oldDayId), newDayId: Number(new_day_id) }, req.headers['x-socket-id'] as string); }); -router.get('/trips/:tripId/assignments/:id/participants', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.get('/trips/:tripId/assignments/:id/participants', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const participants = db.prepare(` @@ -203,7 +203,7 @@ router.get('/trips/:tripId/assignments/:id/participants', authenticate, requireT res.json({ participants }); }); -router.put('/trips/:tripId/assignments/:id/time', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.put('/trips/:tripId/assignments/:id/time', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const assignment = db.prepare(` @@ -222,7 +222,7 @@ router.put('/trips/:tripId/assignments/:id/time', authenticate, requireTripAcces broadcast(Number(tripId), 'assignment:updated', { assignment: updated }, req.headers['x-socket-id'] as string); }); -router.put('/trips/:tripId/assignments/:id/participants', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.put('/trips/:tripId/assignments/:id/participants', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const { user_ids } = req.body; diff --git a/server/src/routes/atlas.ts b/server/src/routes/atlas.ts index d5a7227..12d3873 100644 --- a/server/src/routes/atlas.ts +++ b/server/src/routes/atlas.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from 'express'; import { db } from '../db/database'; import { authenticate } from '../middleware/auth'; -import { AuthRequest, Trip, Place } from '../types'; +import { StringParams, AuthRequest, Trip, Place } from '../types'; const router = express.Router(); router.use(authenticate); @@ -83,7 +83,7 @@ const CONTINENT_MAP: Record = { SE:'Europe',CH:'Europe',TH:'Asia',TR:'Europe',UA:'Europe',AE:'Asia',GB:'Europe',US:'North America',VN:'Asia',NG:'Africa', }; -router.get('/stats', (req: Request, res: Response) => { +router.get('/stats', (req: Request, res: Response) => { const authReq = req as AuthRequest; const userId = authReq.user.id; @@ -208,7 +208,7 @@ router.get('/stats', (req: Request, res: Response) => { }); }); -router.get('/country/:code', (req: Request, res: Response) => { +router.get('/country/:code', (req: Request, res: Response) => { const authReq = req as AuthRequest; const userId = authReq.user.id; const code = req.params.code.toUpperCase(); diff --git a/server/src/routes/backup.ts b/server/src/routes/backup.ts index e18190d..8d88bde 100644 --- a/server/src/routes/backup.ts +++ b/server/src/routes/backup.ts @@ -1,4 +1,5 @@ import express, { Request, Response, NextFunction } from 'express'; +import { StringParams } from '../types'; import archiver from 'archiver'; import unzipper from 'unzipper'; import multer from 'multer'; @@ -17,7 +18,7 @@ const MAX_BACKUP_UPLOAD_SIZE = 500 * 1024 * 1024; // 500 MB const backupAttempts = new Map(); function backupRateLimiter(maxAttempts: number, windowMs: number) { - return (req: Request, res: Response, next: NextFunction) => { + return (req: Request, res: Response, next: NextFunction) => { const key = req.ip || 'unknown'; const now = Date.now(); const record = backupAttempts.get(key); @@ -119,7 +120,7 @@ router.post('/create', backupRateLimiter(3, BACKUP_RATE_WINDOW), async (_req: Re } }); -router.get('/download/:filename', (req: Request, res: Response) => { +router.get('/download/:filename', (req: Request, res: Response) => { const { filename } = req.params; if (!/^backup-[\w\-]+\.zip$/.test(filename)) { @@ -182,7 +183,7 @@ async function restoreFromZip(zipPath: string, res: Response) { } } -router.post('/restore/:filename', async (req: Request, res: Response) => { +router.post('/restore/:filename', async (req: Request, res: Response) => { const { filename } = req.params; if (!/^backup-[\w\-]+\.zip$/.test(filename)) { return res.status(400).json({ error: 'Invalid filename' }); @@ -203,7 +204,7 @@ const uploadTmp = multer({ limits: { fileSize: MAX_BACKUP_UPLOAD_SIZE }, }); -router.post('/upload-restore', uploadTmp.single('backup'), async (req: Request, res: Response) => { +router.post('/upload-restore', uploadTmp.single('backup'), async (req: Request, res: Response) => { if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); const zipPath = req.file.path; await restoreFromZip(zipPath, res); @@ -243,7 +244,7 @@ function parseAutoBackupBody(body: Record): { return { enabled, interval, keep_days }; } -router.put('/auto-settings', (req: Request, res: Response) => { +router.put('/auto-settings', (req: Request, res: Response) => { try { const settings = parseAutoBackupBody((req.body || {}) as Record); scheduler.saveSettings(settings); @@ -259,7 +260,7 @@ router.put('/auto-settings', (req: Request, res: Response) => { } }); -router.delete('/:filename', (req: Request, res: Response) => { +router.delete('/:filename', (req: Request, res: Response) => { const { filename } = req.params; if (!/^backup-[\w\-]+\.zip$/.test(filename)) { diff --git a/server/src/routes/budget.ts b/server/src/routes/budget.ts index c1b9258..c371181 100644 --- a/server/src/routes/budget.ts +++ b/server/src/routes/budget.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import { db, canAccessTrip } from '../db/database'; import { authenticate } from '../middleware/auth'; import { broadcast } from '../websocket'; -import { AuthRequest, BudgetItem, BudgetItemMember } from '../types'; +import { StringParams, AuthRequest, BudgetItem, BudgetItemMember } from '../types'; const router = express.Router({ mergeParams: true }); @@ -23,7 +23,7 @@ function avatarUrl(user: { avatar?: string | null }): string | null { return user.avatar ? `/uploads/avatars/${user.avatar}` : null; } -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; @@ -55,7 +55,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { res.json({ items }); }); -router.get('/summary/per-person', authenticate, (req: Request, res: Response) => { +router.get('/summary/per-person', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; if (!canAccessTrip(Number(tripId), authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -75,7 +75,7 @@ router.get('/summary/per-person', authenticate, (req: Request, res: Response) => res.json({ summary: summary.map(s => ({ ...s, avatar_url: avatarUrl(s) })) }); }); -router.post('/', authenticate, (req: Request, res: Response) => { +router.post('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { category, name, total_price, persons, days, note } = req.body; @@ -107,7 +107,7 @@ router.post('/', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'budget:created', { item }, req.headers['x-socket-id'] as string); }); -router.put('/:id', authenticate, (req: Request, res: Response) => { +router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { category, name, total_price, persons, days, note, sort_order } = req.body; @@ -145,7 +145,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'budget:updated', { item: updated }, req.headers['x-socket-id'] as string); }); -router.put('/:id/members', authenticate, (req: Request, res: Response) => { +router.put('/:id/members', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!canAccessTrip(Number(tripId), authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -175,7 +175,7 @@ router.put('/:id/members', authenticate, (req: Request, res: Response) => { broadcast(Number(tripId), 'budget:members-updated', { itemId: Number(id), members, persons: (updated as BudgetItem).persons }, req.headers['x-socket-id'] as string); }); -router.put('/:id/members/:userId/paid', authenticate, (req: Request, res: Response) => { +router.put('/:id/members/:userId/paid', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id, userId } = req.params; if (!canAccessTrip(Number(tripId), authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -195,7 +195,7 @@ router.put('/:id/members/:userId/paid', authenticate, (req: Request, res: Respon broadcast(Number(tripId), 'budget:member-paid-updated', { itemId: Number(id), userId: Number(userId), paid: paid ? 1 : 0 }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; diff --git a/server/src/routes/collab.ts b/server/src/routes/collab.ts index 3c5d6b2..432d2ed 100644 --- a/server/src/routes/collab.ts +++ b/server/src/routes/collab.ts @@ -7,7 +7,7 @@ import { db, canAccessTrip } from '../db/database'; import { authenticate } from '../middleware/auth'; import { broadcast } from '../websocket'; import { validateStringLengths } from '../middleware/validate'; -import { AuthRequest, CollabNote, CollabPoll, CollabMessage, TripFile } from '../types'; +import { StringParams, AuthRequest, CollabNote, CollabPoll, CollabMessage, TripFile } from '../types'; interface ReactionRow { emoji: string; @@ -90,7 +90,7 @@ function formatMessage(msg: CollabMessage, reactions?: { emoji: string; users: { return { ...msg, user_avatar: avatarUrl(msg), avatar_url: avatarUrl(msg), reactions: reactions || [] }; } -router.get('/notes', authenticate, (req: Request, res: Response) => { +router.get('/notes', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -106,7 +106,7 @@ router.get('/notes', authenticate, (req: Request, res: Response) => { res.json({ notes: notes.map(formatNote) }); }); -router.post('/notes', authenticate, (req: Request, res: Response) => { +router.post('/notes', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { title, content, category, color, website } = req.body; @@ -127,7 +127,7 @@ router.post('/notes', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'collab:note:created', { note: formatted }, req.headers['x-socket-id'] as string); }); -router.put('/notes/:id', authenticate, (req: Request, res: Response) => { +router.put('/notes/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { title, content, category, color, pinned, website } = req.body; @@ -165,7 +165,7 @@ router.put('/notes/:id', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'collab:note:updated', { note: formatted }, req.headers['x-socket-id'] as string); }); -router.delete('/notes/:id', authenticate, (req: Request, res: Response) => { +router.delete('/notes/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -185,7 +185,7 @@ router.delete('/notes/:id', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'collab:note:deleted', { noteId: Number(id) }, req.headers['x-socket-id'] as string); }); -router.post('/notes/:id/files', authenticate, noteUpload.single('file'), (req: Request, res: Response) => { +router.post('/notes/:id/files', authenticate, noteUpload.single('file'), (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!verifyTripAccess(Number(tripId), authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -203,7 +203,7 @@ router.post('/notes/:id/files', authenticate, noteUpload.single('file'), (req: R broadcast(Number(tripId), 'collab:note:updated', { note: formatNote(db.prepare('SELECT n.*, u.username, u.avatar FROM collab_notes n JOIN users u ON n.user_id = u.id WHERE n.id = ?').get(id) as CollabNote) }, req.headers['x-socket-id'] as string); }); -router.delete('/notes/:id/files/:fileId', authenticate, (req: Request, res: Response) => { +router.delete('/notes/:id/files/:fileId', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id, fileId } = req.params; if (!verifyTripAccess(Number(tripId), authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -254,7 +254,7 @@ function getPollWithVotes(pollId: number | bigint | string) { }; } -router.get('/polls', authenticate, (req: Request, res: Response) => { +router.get('/polls', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -267,7 +267,7 @@ router.get('/polls', authenticate, (req: Request, res: Response) => { res.json({ polls }); }); -router.post('/polls', authenticate, (req: Request, res: Response) => { +router.post('/polls', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { question, options, multiple, multiple_choice, deadline } = req.body; @@ -289,7 +289,7 @@ router.post('/polls', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'collab:poll:created', { poll }, req.headers['x-socket-id'] as string); }); -router.post('/polls/:id/vote', authenticate, (req: Request, res: Response) => { +router.post('/polls/:id/vote', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { option_index } = req.body; @@ -322,7 +322,7 @@ router.post('/polls/:id/vote', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'collab:poll:voted', { poll: updatedPoll }, req.headers['x-socket-id'] as string); }); -router.put('/polls/:id/close', authenticate, (req: Request, res: Response) => { +router.put('/polls/:id/close', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -337,7 +337,7 @@ router.put('/polls/:id/close', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'collab:poll:closed', { poll: updatedPoll }, req.headers['x-socket-id'] as string); }); -router.delete('/polls/:id', authenticate, (req: Request, res: Response) => { +router.delete('/polls/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -350,7 +350,7 @@ router.delete('/polls/:id', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'collab:poll:deleted', { pollId: Number(id) }, req.headers['x-socket-id'] as string); }); -router.get('/messages', authenticate, (req: Request, res: Response) => { +router.get('/messages', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { before } = req.query; @@ -390,7 +390,7 @@ router.get('/messages', authenticate, (req: Request, res: Response) => { res.json({ messages: messages.map(m => formatMessage(m, groupReactions(reactionsByMsg[m.id] || []))) }); }); -router.post('/messages', authenticate, validateStringLengths({ text: 5000 }), (req: Request, res: Response) => { +router.post('/messages', authenticate, validateStringLengths({ text: 5000 }), (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { text, reply_to } = req.body; @@ -421,7 +421,7 @@ router.post('/messages', authenticate, validateStringLengths({ text: 5000 }), (r broadcast(tripId, 'collab:message:created', { message: formatted }, req.headers['x-socket-id'] as string); }); -router.post('/messages/:id/react', authenticate, (req: Request, res: Response) => { +router.post('/messages/:id/react', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { emoji } = req.body; @@ -443,7 +443,7 @@ router.post('/messages/:id/react', authenticate, (req: Request, res: Response) = broadcast(Number(tripId), 'collab:message:reacted', { messageId: Number(id), reactions }, req.headers['x-socket-id'] as string); }); -router.delete('/messages/:id', authenticate, (req: Request, res: Response) => { +router.delete('/messages/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -457,7 +457,7 @@ router.delete('/messages/:id', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'collab:message:deleted', { messageId: Number(id), username: message.username || authReq.user.username }, req.headers['x-socket-id'] as string); }); -router.get('/link-preview', authenticate, async (req: Request, res: Response) => { +router.get('/link-preview', authenticate, async (req: Request, res: Response) => { const { url } = req.query as { url?: string }; if (!url) return res.status(400).json({ error: 'URL is required' }); diff --git a/server/src/routes/dayNotes.ts b/server/src/routes/dayNotes.ts index 1605c8e..ce2abd5 100644 --- a/server/src/routes/dayNotes.ts +++ b/server/src/routes/dayNotes.ts @@ -3,7 +3,7 @@ import { db, canAccessTrip } from '../db/database'; import { authenticate } from '../middleware/auth'; import { broadcast } from '../websocket'; import { validateStringLengths } from '../middleware/validate'; -import { AuthRequest, DayNote } from '../types'; +import { StringParams, AuthRequest, DayNote } from '../types'; const router = express.Router({ mergeParams: true }); @@ -11,7 +11,7 @@ function verifyAccess(tripId: string | number, userId: number) { return canAccessTrip(tripId, userId); } -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, dayId } = req.params; if (!verifyAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -23,7 +23,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { res.json({ notes }); }); -router.post('/', authenticate, validateStringLengths({ text: 500, time: 150 }), (req: Request, res: Response) => { +router.post('/', authenticate, validateStringLengths({ text: 500, time: 150 }), (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, dayId } = req.params; if (!verifyAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -43,7 +43,7 @@ router.post('/', authenticate, validateStringLengths({ text: 500, time: 150 }), broadcast(tripId, 'dayNote:created', { dayId: Number(dayId), note }, req.headers['x-socket-id'] as string); }); -router.put('/:id', authenticate, validateStringLengths({ text: 500, time: 150 }), (req: Request, res: Response) => { +router.put('/:id', authenticate, validateStringLengths({ text: 500, time: 150 }), (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, dayId, id } = req.params; if (!verifyAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -67,7 +67,7 @@ router.put('/:id', authenticate, validateStringLengths({ text: 500, time: 150 }) broadcast(tripId, 'dayNote:updated', { dayId: Number(dayId), note: updated }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, dayId, id } = req.params; if (!verifyAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); diff --git a/server/src/routes/days.ts b/server/src/routes/days.ts index c59e1b7..de69f5c 100644 --- a/server/src/routes/days.ts +++ b/server/src/routes/days.ts @@ -4,7 +4,7 @@ import { authenticate } from '../middleware/auth'; import { requireTripAccess } from '../middleware/tripAccess'; import { broadcast } from '../websocket'; import { loadTagsByPlaceIds, loadParticipantsByAssignmentIds, formatAssignmentWithPlace } from '../services/queryHelpers'; -import { AuthRequest, AssignmentRow, Day, DayNote } from '../types'; +import { StringParams, AuthRequest, AssignmentRow, Day, DayNote } from '../types'; const router = express.Router({ mergeParams: true }); @@ -68,7 +68,7 @@ function getAssignmentsForDay(dayId: number | string) { }); } -router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId } = req.params; const days = db.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(tripId) as Day[]; @@ -125,7 +125,7 @@ router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) = res.json({ days: daysWithAssignments }); }); -router.post('/', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.post('/', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId } = req.params; const { date, notes } = req.body; @@ -143,7 +143,7 @@ router.post('/', authenticate, requireTripAccess, (req: Request, res: Response) broadcast(tripId, 'day:created', { day: dayResult }, req.headers['x-socket-id'] as string); }); -router.put('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.put('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const day = db.prepare('SELECT * FROM days WHERE id = ? AND trip_id = ?').get(id, tripId) as Day | undefined; @@ -160,7 +160,7 @@ router.put('/:id', authenticate, requireTripAccess, (req: Request, res: Response broadcast(tripId, 'day:updated', { day: dayWithAssignments }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const day = db.prepare('SELECT * FROM days WHERE id = ? AND trip_id = ?').get(id, tripId); @@ -184,7 +184,7 @@ function getAccommodationWithPlace(id: number | bigint) { `).get(id); } -accommodationsRouter.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { +accommodationsRouter.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId } = req.params; const accommodations = db.prepare(` @@ -198,7 +198,7 @@ accommodationsRouter.get('/', authenticate, requireTripAccess, (req: Request, re res.json({ accommodations }); }); -accommodationsRouter.post('/', authenticate, requireTripAccess, (req: Request, res: Response) => { +accommodationsRouter.post('/', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId } = req.params; const { place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes } = req.body; @@ -242,7 +242,7 @@ accommodationsRouter.post('/', authenticate, requireTripAccess, (req: Request, r broadcast(tripId, 'reservation:created', {}, req.headers['x-socket-id'] as string); }); -accommodationsRouter.put('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +accommodationsRouter.put('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; interface DayAccommodation { id: number; trip_id: number; place_id: number; start_day_id: number; end_day_id: number; check_in: string | null; check_out: string | null; confirmation: string | null; notes: string | null; } @@ -293,7 +293,7 @@ accommodationsRouter.put('/:id', authenticate, requireTripAccess, (req: Request, broadcast(tripId, 'accommodation:updated', { accommodation }, req.headers['x-socket-id'] as string); }); -accommodationsRouter.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +accommodationsRouter.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const existing = db.prepare('SELECT * FROM day_accommodations WHERE id = ? AND trip_id = ?').get(id, tripId); diff --git a/server/src/routes/files.ts b/server/src/routes/files.ts index 378f074..7fa48db 100644 --- a/server/src/routes/files.ts +++ b/server/src/routes/files.ts @@ -7,7 +7,7 @@ import { db, canAccessTrip } from '../db/database'; import { authenticate, demoUploadBlock } from '../middleware/auth'; import { requireTripAccess } from '../middleware/tripAccess'; import { broadcast } from '../websocket'; -import { AuthRequest, TripFile } from '../types'; +import { StringParams, AuthRequest, TripFile } from '../types'; const router = express.Router({ mergeParams: true }); @@ -72,7 +72,7 @@ function formatFile(file: TripFile) { } // List files (excludes soft-deleted by default) -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const showTrash = req.query.trash === 'true'; @@ -86,7 +86,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { }); // Upload file -router.post('/', authenticate, requireTripAccess, demoUploadBlock, upload.single('file'), (req: Request, res: Response) => { +router.post('/', authenticate, requireTripAccess, demoUploadBlock, upload.single('file'), (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { place_id, description, reservation_id } = req.body; @@ -116,7 +116,7 @@ router.post('/', authenticate, requireTripAccess, demoUploadBlock, upload.single }); // Update file metadata -router.put('/:id', authenticate, (req: Request, res: Response) => { +router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { description, place_id, reservation_id } = req.body; @@ -146,7 +146,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => { }); // Toggle starred -router.patch('/:id/star', authenticate, (req: Request, res: Response) => { +router.patch('/:id/star', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; @@ -165,7 +165,7 @@ router.patch('/:id/star', authenticate, (req: Request, res: Response) => { }); // Soft-delete (move to trash) -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; @@ -181,7 +181,7 @@ router.delete('/:id', authenticate, (req: Request, res: Response) => { }); // Restore from trash -router.post('/:id/restore', authenticate, (req: Request, res: Response) => { +router.post('/:id/restore', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; @@ -199,7 +199,7 @@ router.post('/:id/restore', authenticate, (req: Request, res: Response) => { }); // Permanently delete from trash -router.delete('/:id/permanent', authenticate, (req: Request, res: Response) => { +router.delete('/:id/permanent', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; @@ -220,7 +220,7 @@ router.delete('/:id/permanent', authenticate, (req: Request, res: Response) => { }); // Empty entire trash -router.delete('/trash/empty', authenticate, (req: Request, res: Response) => { +router.delete('/trash/empty', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; diff --git a/server/src/routes/maps.ts b/server/src/routes/maps.ts index b71e80d..97388d7 100644 --- a/server/src/routes/maps.ts +++ b/server/src/routes/maps.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import fetch from 'node-fetch'; import { db } from '../db/database'; import { authenticate } from '../middleware/auth'; -import { AuthRequest } from '../types'; +import { StringParams, AuthRequest } from '../types'; interface NominatimResult { osm_type: string; @@ -247,7 +247,7 @@ async function searchNominatim(query: string, lang?: string) { })); } -router.post('/search', authenticate, async (req: Request, res: Response) => { +router.post('/search', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { query } = req.body; @@ -301,7 +301,7 @@ router.post('/search', authenticate, async (req: Request, res: Response) => { } }); -router.get('/details/:placeId', authenticate, async (req: Request, res: Response) => { +router.get('/details/:placeId', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { placeId } = req.params; @@ -372,7 +372,7 @@ router.get('/details/:placeId', authenticate, async (req: Request, res: Response } }); -router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Response) => { +router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { placeId } = req.params; @@ -453,7 +453,7 @@ router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Resp }); // Reverse geocoding via Nominatim -router.get('/reverse', authenticate, async (req: Request, res: Response) => { +router.get('/reverse', authenticate, async (req: Request, res: Response) => { const { lat, lng, lang } = req.query as { lat: string; lng: string; lang?: string }; if (!lat || !lng) return res.status(400).json({ error: 'lat and lng required' }); try { diff --git a/server/src/routes/packing.ts b/server/src/routes/packing.ts index 22cb7b7..fac3e39 100644 --- a/server/src/routes/packing.ts +++ b/server/src/routes/packing.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import { db, canAccessTrip } from '../db/database'; import { authenticate } from '../middleware/auth'; import { broadcast } from '../websocket'; -import { AuthRequest } from '../types'; +import { StringParams, AuthRequest } from '../types'; const router = express.Router({ mergeParams: true }); @@ -10,7 +10,7 @@ function verifyTripOwnership(tripId: string | number, userId: number) { return canAccessTrip(tripId, userId); } -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; @@ -24,7 +24,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { res.json({ items }); }); -router.post('/', authenticate, (req: Request, res: Response) => { +router.post('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { name, category, checked } = req.body; @@ -46,7 +46,7 @@ router.post('/', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'packing:created', { item }, req.headers['x-socket-id'] as string); }); -router.put('/:id', authenticate, (req: Request, res: Response) => { +router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { name, checked, category } = req.body; @@ -76,7 +76,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'packing:updated', { item: updated }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; @@ -91,7 +91,7 @@ router.delete('/:id', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'packing:deleted', { itemId: Number(id) }, req.headers['x-socket-id'] as string); }); -router.put('/reorder', authenticate, (req: Request, res: Response) => { +router.put('/reorder', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { orderedIds } = req.body; diff --git a/server/src/routes/places.ts b/server/src/routes/places.ts index 8dae83d..d2704ea 100644 --- a/server/src/routes/places.ts +++ b/server/src/routes/places.ts @@ -6,7 +6,7 @@ import { requireTripAccess } from '../middleware/tripAccess'; import { broadcast } from '../websocket'; import { loadTagsByPlaceIds } from '../services/queryHelpers'; import { validateStringLengths } from '../middleware/validate'; -import { AuthRequest, Place } from '../types'; +import { StringParams, AuthRequest, Place } from '../types'; interface PlaceWithCategory extends Place { category_name: string | null; @@ -21,8 +21,8 @@ interface UnsplashSearchResponse { const router = express.Router({ mergeParams: true }); -router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { - const { tripId } = req.params; +router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { + const { tripId } = req.params const { search, category, tag } = req.query; let query = ` @@ -72,8 +72,8 @@ router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) = res.json({ places: placesWithTags }); }); -router.post('/', authenticate, requireTripAccess, validateStringLengths({ name: 200, description: 2000, address: 500, notes: 2000 }), (req: Request, res: Response) => { - const { tripId } = req.params; +router.post('/', authenticate, requireTripAccess, validateStringLengths({ name: 200, description: 2000, address: 500, notes: 2000 }), (req: Request, res: Response) => { + const { tripId } = req.params const { name, description, lat, lng, address, category_id, price, currency, @@ -112,8 +112,8 @@ router.post('/', authenticate, requireTripAccess, validateStringLengths({ name: broadcast(tripId, 'place:created', { place }, req.headers['x-socket-id'] as string); }); -router.get('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { - const { tripId, id } = req.params; +router.get('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { + const { tripId, id } = req.params const placeCheck = db.prepare('SELECT id FROM places WHERE id = ? AND trip_id = ?').get(id, tripId); if (!placeCheck) { @@ -124,9 +124,9 @@ router.get('/:id', authenticate, requireTripAccess, (req: Request, res: Response res.json({ place }); }); -router.get('/:id/image', authenticate, requireTripAccess, async (req: Request, res: Response) => { +router.get('/:id/image', authenticate, requireTripAccess, async (req: Request, res: Response) => { const authReq = req as AuthRequest; - const { tripId, id } = req.params; + const { tripId, id } = req.params const place = db.prepare('SELECT * FROM places WHERE id = ? AND trip_id = ?').get(id, tripId) as Place | undefined; if (!place) { @@ -165,8 +165,8 @@ router.get('/:id/image', authenticate, requireTripAccess, async (req: Request, r } }); -router.put('/:id', authenticate, requireTripAccess, validateStringLengths({ name: 200, description: 2000, address: 500, notes: 2000 }), (req: Request, res: Response) => { - const { tripId, id } = req.params; +router.put('/:id', authenticate, requireTripAccess, validateStringLengths({ name: 200, description: 2000, address: 500, notes: 2000 }), (req: Request, res: Response) => { + const { tripId, id } = req.params const existingPlace = db.prepare('SELECT * FROM places WHERE id = ? AND trip_id = ?').get(id, tripId) as Place | undefined; if (!existingPlace) { @@ -237,8 +237,8 @@ router.put('/:id', authenticate, requireTripAccess, validateStringLengths({ name broadcast(tripId, 'place:updated', { place }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { - const { tripId, id } = req.params; +router.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { + const { tripId, id } = req.params const place = db.prepare('SELECT id FROM places WHERE id = ? AND trip_id = ?').get(id, tripId); if (!place) { diff --git a/server/src/routes/reservations.ts b/server/src/routes/reservations.ts index 302c1a1..315e39c 100644 --- a/server/src/routes/reservations.ts +++ b/server/src/routes/reservations.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import { db, canAccessTrip } from '../db/database'; import { authenticate } from '../middleware/auth'; import { broadcast } from '../websocket'; -import { AuthRequest, Reservation } from '../types'; +import { StringParams, AuthRequest, Reservation } from '../types'; const router = express.Router({ mergeParams: true }); @@ -10,7 +10,7 @@ function verifyTripOwnership(tripId: string | number, userId: number) { return canAccessTrip(tripId, userId); } -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; @@ -32,7 +32,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { res.json({ reservations }); }); -router.post('/', authenticate, (req: Request, res: Response) => { +router.post('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type, accommodation_id, metadata, create_accommodation } = req.body; @@ -103,7 +103,7 @@ router.post('/', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'reservation:created', { reservation }, req.headers['x-socket-id'] as string); }); -router.put('/:id', authenticate, (req: Request, res: Response) => { +router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type, accommodation_id, metadata, create_accommodation } = req.body; @@ -195,7 +195,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'reservation:updated', { reservation: updated }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; diff --git a/server/src/routes/trips.ts b/server/src/routes/trips.ts index ad5231d..05ffea9 100644 --- a/server/src/routes/trips.ts +++ b/server/src/routes/trips.ts @@ -6,7 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { db, canAccessTrip, isOwner } from '../db/database'; import { authenticate, demoUploadBlock } from '../middleware/auth'; import { broadcast } from '../websocket'; -import { AuthRequest, Trip, User } from '../types'; +import { StringParams, AuthRequest, Trip, User } from '../types'; const router = express.Router(); @@ -120,7 +120,7 @@ function generateDays(tripId: number | bigint | string, startDate: string | null } } -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const archived = req.query.archived === '1' ? 1 : 0; const userId = authReq.user.id; @@ -133,7 +133,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { res.json({ trips }); }); -router.post('/', authenticate, (req: Request, res: Response) => { +router.post('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { title, description, start_date, end_date, currency } = req.body; if (!title) return res.status(400).json({ error: 'Title is required' }); @@ -151,7 +151,7 @@ router.post('/', authenticate, (req: Request, res: Response) => { res.status(201).json({ trip }); }); -router.get('/:id', authenticate, (req: Request, res: Response) => { +router.get('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const userId = authReq.user.id; const trip = db.prepare(` @@ -163,7 +163,7 @@ router.get('/:id', authenticate, (req: Request, res: Response) => { res.json({ trip }); }); -router.put('/:id', authenticate, (req: Request, res: Response) => { +router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const access = canAccessTrip(req.params.id, authReq.user.id); if (!access) return res.status(404).json({ error: 'Trip not found' }); @@ -201,7 +201,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => { broadcast(req.params.id, 'trip:updated', { trip: updatedTrip }, req.headers['x-socket-id'] as string); }); -router.post('/:id/cover', authenticate, demoUploadBlock, uploadCover.single('cover'), (req: Request, res: Response) => { +router.post('/:id/cover', authenticate, demoUploadBlock, uploadCover.single('cover'), (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!isOwner(req.params.id, authReq.user.id)) return res.status(403).json({ error: 'Only the owner can change the cover image' }); @@ -224,7 +224,7 @@ router.post('/:id/cover', authenticate, demoUploadBlock, uploadCover.single('cov res.json({ cover_image: coverUrl }); }); -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!isOwner(req.params.id, authReq.user.id)) return res.status(403).json({ error: 'Only the owner can delete the trip' }); @@ -234,7 +234,7 @@ router.delete('/:id', authenticate, (req: Request, res: Response) => { broadcast(deletedTripId, 'trip:deleted', { id: deletedTripId }, req.headers['x-socket-id'] as string); }); -router.get('/:id/members', authenticate, (req: Request, res: Response) => { +router.get('/:id/members', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!canAccessTrip(req.params.id, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -261,7 +261,7 @@ router.get('/:id/members', authenticate, (req: Request, res: Response) => { }); }); -router.post('/:id/members', authenticate, (req: Request, res: Response) => { +router.post('/:id/members', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!canAccessTrip(req.params.id, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -287,7 +287,7 @@ router.post('/:id/members', authenticate, (req: Request, res: Response) => { res.status(201).json({ member: { ...target, role: 'member', avatar_url: target.avatar ? `/uploads/avatars/${target.avatar}` : null } }); }); -router.delete('/:id/members/:userId', authenticate, (req: Request, res: Response) => { +router.delete('/:id/members/:userId', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!canAccessTrip(req.params.id, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); diff --git a/server/src/routes/vacay.ts b/server/src/routes/vacay.ts index 02b13d9..a645cdb 100644 --- a/server/src/routes/vacay.ts +++ b/server/src/routes/vacay.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from 'express'; import { db } from '../db/database'; import { authenticate } from '../middleware/auth'; -import { AuthRequest } from '../types'; +import { StringParams, AuthRequest } from '../types'; interface VacayPlan { id: number; @@ -100,7 +100,7 @@ function getPlanUsers(planId: number) { return [owner, ...members]; } -router.get('/plan', (req: Request, res: Response) => { +router.get('/plan', (req: Request, res: Response) => { const authReq = req as AuthRequest; const plan = getActivePlan(authReq.user.id); const activePlanId = plan.id; @@ -140,7 +140,7 @@ router.get('/plan', (req: Request, res: Response) => { }); }); -router.put('/plan', async (req: Request, res: Response) => { +router.put('/plan', async (req: Request, res: Response) => { const authReq = req as AuthRequest; const planId = getActivePlanId(authReq.user.id); const { block_weekends, holidays_enabled, holidays_region, company_holidays_enabled, carry_over_enabled } = req.body; @@ -222,7 +222,7 @@ router.put('/plan', async (req: Request, res: Response) => { }); }); -router.put('/color', (req: Request, res: Response) => { +router.put('/color', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { color, target_user_id } = req.body; const planId = getActivePlanId(authReq.user.id); @@ -239,7 +239,7 @@ router.put('/color', (req: Request, res: Response) => { res.json({ success: true }); }); -router.post('/invite', (req: Request, res: Response) => { +router.post('/invite', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { user_id } = req.body; if (!user_id) return res.status(400).json({ error: 'user_id required' }); @@ -273,7 +273,7 @@ router.post('/invite', (req: Request, res: Response) => { res.json({ success: true }); }); -router.post('/invite/accept', (req: Request, res: Response) => { +router.post('/invite/accept', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { plan_id } = req.body; const invite = db.prepare("SELECT * FROM vacay_plan_members WHERE plan_id = ? AND user_id = ? AND status = 'pending'").get(plan_id, authReq.user.id) as VacayPlanMember | undefined; @@ -318,7 +318,7 @@ router.post('/invite/accept', (req: Request, res: Response) => { res.json({ success: true }); }); -router.post('/invite/decline', (req: Request, res: Response) => { +router.post('/invite/decline', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { plan_id } = req.body; db.prepare("DELETE FROM vacay_plan_members WHERE plan_id = ? AND user_id = ? AND status = 'pending'").run(plan_id, authReq.user.id); @@ -326,7 +326,7 @@ router.post('/invite/decline', (req: Request, res: Response) => { res.json({ success: true }); }); -router.post('/invite/cancel', (req: Request, res: Response) => { +router.post('/invite/cancel', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { user_id } = req.body; const plan = getActivePlan(authReq.user.id); @@ -340,7 +340,7 @@ router.post('/invite/cancel', (req: Request, res: Response) => { res.json({ success: true }); }); -router.post('/dissolve', (req: Request, res: Response) => { +router.post('/dissolve', (req: Request, res: Response) => { const authReq = req as AuthRequest; const plan = getActivePlan(authReq.user.id); const isOwnerFlag = plan.owner_id === authReq.user.id; @@ -375,7 +375,7 @@ router.post('/dissolve', (req: Request, res: Response) => { res.json({ success: true }); }); -router.get('/available-users', (req: Request, res: Response) => { +router.get('/available-users', (req: Request, res: Response) => { const authReq = req as AuthRequest; const planId = getActivePlanId(authReq.user.id); const users = db.prepare(` @@ -391,14 +391,14 @@ router.get('/available-users', (req: Request, res: Response) => { res.json({ users }); }); -router.get('/years', (req: Request, res: Response) => { +router.get('/years', (req: Request, res: Response) => { const authReq = req as AuthRequest; const planId = getActivePlanId(authReq.user.id); const years = db.prepare('SELECT year FROM vacay_years WHERE plan_id = ? ORDER BY year').all(planId) as { year: number }[]; res.json({ years: years.map(y => y.year) }); }); -router.post('/years', (req: Request, res: Response) => { +router.post('/years', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { year } = req.body; if (!year) return res.status(400).json({ error: 'Year required' }); @@ -426,7 +426,7 @@ router.post('/years', (req: Request, res: Response) => { res.json({ years: years.map(y => y.year) }); }); -router.delete('/years/:year', (req: Request, res: Response) => { +router.delete('/years/:year', (req: Request, res: Response) => { const authReq = req as AuthRequest; const year = parseInt(req.params.year); const planId = getActivePlanId(authReq.user.id); @@ -438,7 +438,7 @@ router.delete('/years/:year', (req: Request, res: Response) => { res.json({ years: years.map(y => y.year) }); }); -router.get('/entries/:year', (req: Request, res: Response) => { +router.get('/entries/:year', (req: Request, res: Response) => { const authReq = req as AuthRequest; const year = req.params.year; const planId = getActivePlanId(authReq.user.id); @@ -453,7 +453,7 @@ router.get('/entries/:year', (req: Request, res: Response) => { res.json({ entries, companyHolidays }); }); -router.post('/entries/toggle', (req: Request, res: Response) => { +router.post('/entries/toggle', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { date, target_user_id } = req.body; if (!date) return res.status(400).json({ error: 'date required' }); @@ -479,7 +479,7 @@ router.post('/entries/toggle', (req: Request, res: Response) => { } }); -router.post('/entries/company-holiday', (req: Request, res: Response) => { +router.post('/entries/company-holiday', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { date, note } = req.body; const planId = getActivePlanId(authReq.user.id); @@ -496,7 +496,7 @@ router.post('/entries/company-holiday', (req: Request, res: Response) => { } }); -router.get('/stats/:year', (req: Request, res: Response) => { +router.get('/stats/:year', (req: Request, res: Response) => { const authReq = req as AuthRequest; const year = parseInt(req.params.year); const planId = getActivePlanId(authReq.user.id); @@ -532,7 +532,7 @@ router.get('/stats/:year', (req: Request, res: Response) => { res.json({ stats }); }); -router.put('/stats/:year', (req: Request, res: Response) => { +router.put('/stats/:year', (req: Request, res: Response) => { const authReq = req as AuthRequest; const year = parseInt(req.params.year); const { vacation_days, target_user_id } = req.body; @@ -564,7 +564,7 @@ router.get('/holidays/countries', async (_req: Request, res: Response) => { } }); -router.get('/holidays/:year/:country', async (req: Request, res: Response) => { +router.get('/holidays/:year/:country', async (req: Request, res: Response) => { const { year, country } = req.params; const cacheKey = `${year}-${country}`; const cached = holidayCache.get(cacheKey); diff --git a/server/src/types.ts b/server/src/types.ts index b9f2142..190ce0a 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,5 +1,9 @@ import { Request } from 'express'; +// Route params from Express 4 are always strings at runtime. +// Use this with Request to get string-typed req.params. +export type StringParams = Record; + export interface User { id: number; username: string; diff --git a/server/tsconfig.json b/server/tsconfig.json index 4659864..0ecff37 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -9,7 +9,6 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "types": ["node"], "resolveJsonModule": true, "declaration": false, "sourceMap": true, From 3d3319192582c41979dc026a98c8e364967aceb4 Mon Sep 17 00:00:00 2001 From: Stephen Wheet Date: Sat, 28 Mar 2026 20:36:09 +0000 Subject: [PATCH 5/5] fix: align @types/express to v4 to match express runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The project uses express@^4.18.3 at runtime but had @types/express@^5.0.6 as type definitions. The v5 types widened ParamsDictionary from string to string | string[], causing 115 type errors across all route handlers. Fix: downgrade @types/express to ^4.17.25 (latest v4), which correctly types req.params as string — matching Express 4 runtime behaviour. Removes the StringParams = Record workaround from types.ts and the Request annotations from all 15 route files that were introduced as a workaround for the type mismatch. --- server/package-lock.json | 44 ++++++++++++++++++++++--------- server/package.json | 2 +- server/src/index.ts | 3 +-- server/src/routes/assignments.ts | 18 ++++++------- server/src/routes/atlas.ts | 6 ++--- server/src/routes/backup.ts | 13 +++++---- server/src/routes/budget.ts | 16 +++++------ server/src/routes/collab.ts | 34 ++++++++++++------------ server/src/routes/dayNotes.ts | 10 +++---- server/src/routes/days.ts | 18 ++++++------- server/src/routes/files.ts | 18 ++++++------- server/src/routes/maps.ts | 10 +++---- server/src/routes/packing.ts | 12 ++++----- server/src/routes/places.ts | 14 +++++----- server/src/routes/reservations.ts | 10 +++---- server/src/routes/trips.ts | 20 +++++++------- server/src/routes/vacay.ts | 38 +++++++++++++------------- server/src/types.ts | 4 --- 18 files changed, 152 insertions(+), 138 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 77c8247..4f8b27d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -30,7 +30,7 @@ "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", + "@types/express": "^4.17.25", "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.1.0", "@types/node": "^25.5.0", @@ -516,21 +516,22 @@ } }, "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "dev": true, "license": "MIT", "dependencies": { @@ -558,6 +559,13 @@ "@types/node": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -627,13 +635,25 @@ } }, "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", "@types/node": "*" } }, diff --git a/server/package.json b/server/package.json index 39ae754..becc76e 100644 --- a/server/package.json +++ b/server/package.json @@ -29,7 +29,7 @@ "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", + "@types/express": "^4.17.25", "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.1.0", "@types/node": "^25.5.0", diff --git a/server/src/index.ts b/server/src/index.ts index 203ec47..7b1b09a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,6 +1,5 @@ import 'dotenv/config'; import express, { Request, Response, NextFunction } from 'express'; -import { StringParams } from './types'; import cors from 'cors'; import helmet from 'helmet'; import path from 'path'; @@ -79,7 +78,7 @@ app.use(express.urlencoded({ extended: true })); app.use('/uploads/avatars', express.static(path.join(__dirname, '../uploads/avatars'))); // All other uploads require authentication -app.get('/uploads/:type/:filename', (req: Request, res: Response) => { +app.get('/uploads/:type/:filename', (req: Request, res: Response) => { const { type, filename } = req.params; const allowedTypes = ['covers', 'files', 'photos']; if (!allowedTypes.includes(type)) return res.status(404).send('Not found'); diff --git a/server/src/routes/assignments.ts b/server/src/routes/assignments.ts index f84e269..cb2d9a9 100644 --- a/server/src/routes/assignments.ts +++ b/server/src/routes/assignments.ts @@ -4,7 +4,7 @@ import { authenticate } from '../middleware/auth'; import { requireTripAccess } from '../middleware/tripAccess'; import { broadcast } from '../websocket'; import { loadTagsByPlaceIds, loadParticipantsByAssignmentIds, formatAssignmentWithPlace } from '../services/queryHelpers'; -import { StringParams, AuthRequest, AssignmentRow, DayAssignment, Tag, Participant } from '../types'; +import { AuthRequest, AssignmentRow, DayAssignment, Tag, Participant } from '../types'; const router = express.Router({ mergeParams: true }); @@ -75,7 +75,7 @@ function getAssignmentWithPlace(assignmentId: number | bigint) { }; } -router.get('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.get('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, dayId } = req.params; const day = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(dayId, tripId); @@ -109,7 +109,7 @@ router.get('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripAc res.json({ assignments: result }); }); -router.post('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.post('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, dayId } = req.params; const { place_id, notes } = req.body; @@ -131,7 +131,7 @@ router.post('/trips/:tripId/days/:dayId/assignments', authenticate, requireTripA broadcast(tripId, 'assignment:created', { assignment }, req.headers['x-socket-id'] as string); }); -router.delete('/trips/:tripId/days/:dayId/assignments/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.delete('/trips/:tripId/days/:dayId/assignments/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, dayId, id } = req.params; const assignment = db.prepare( @@ -145,7 +145,7 @@ router.delete('/trips/:tripId/days/:dayId/assignments/:id', authenticate, requir broadcast(tripId, 'assignment:deleted', { assignmentId: Number(id), dayId: Number(dayId) }, req.headers['x-socket-id'] as string); }); -router.put('/trips/:tripId/days/:dayId/assignments/reorder', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.put('/trips/:tripId/days/:dayId/assignments/reorder', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, dayId } = req.params; const { orderedIds } = req.body; @@ -167,7 +167,7 @@ router.put('/trips/:tripId/days/:dayId/assignments/reorder', authenticate, requi broadcast(tripId, 'assignment:reordered', { dayId: Number(dayId), orderedIds }, req.headers['x-socket-id'] as string); }); -router.put('/trips/:tripId/assignments/:id/move', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.put('/trips/:tripId/assignments/:id/move', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const { new_day_id, order_index } = req.body; @@ -190,7 +190,7 @@ router.put('/trips/:tripId/assignments/:id/move', authenticate, requireTripAcces broadcast(tripId, 'assignment:moved', { assignment: updated, oldDayId: Number(oldDayId), newDayId: Number(new_day_id) }, req.headers['x-socket-id'] as string); }); -router.get('/trips/:tripId/assignments/:id/participants', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.get('/trips/:tripId/assignments/:id/participants', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const participants = db.prepare(` @@ -203,7 +203,7 @@ router.get('/trips/:tripId/assignments/:id/participants', authenticate, requireT res.json({ participants }); }); -router.put('/trips/:tripId/assignments/:id/time', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.put('/trips/:tripId/assignments/:id/time', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const assignment = db.prepare(` @@ -222,7 +222,7 @@ router.put('/trips/:tripId/assignments/:id/time', authenticate, requireTripAcces broadcast(Number(tripId), 'assignment:updated', { assignment: updated }, req.headers['x-socket-id'] as string); }); -router.put('/trips/:tripId/assignments/:id/participants', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.put('/trips/:tripId/assignments/:id/participants', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const { user_ids } = req.body; diff --git a/server/src/routes/atlas.ts b/server/src/routes/atlas.ts index 12d3873..d5a7227 100644 --- a/server/src/routes/atlas.ts +++ b/server/src/routes/atlas.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from 'express'; import { db } from '../db/database'; import { authenticate } from '../middleware/auth'; -import { StringParams, AuthRequest, Trip, Place } from '../types'; +import { AuthRequest, Trip, Place } from '../types'; const router = express.Router(); router.use(authenticate); @@ -83,7 +83,7 @@ const CONTINENT_MAP: Record = { SE:'Europe',CH:'Europe',TH:'Asia',TR:'Europe',UA:'Europe',AE:'Asia',GB:'Europe',US:'North America',VN:'Asia',NG:'Africa', }; -router.get('/stats', (req: Request, res: Response) => { +router.get('/stats', (req: Request, res: Response) => { const authReq = req as AuthRequest; const userId = authReq.user.id; @@ -208,7 +208,7 @@ router.get('/stats', (req: Request, res: Response) => { }); }); -router.get('/country/:code', (req: Request, res: Response) => { +router.get('/country/:code', (req: Request, res: Response) => { const authReq = req as AuthRequest; const userId = authReq.user.id; const code = req.params.code.toUpperCase(); diff --git a/server/src/routes/backup.ts b/server/src/routes/backup.ts index 8d88bde..e18190d 100644 --- a/server/src/routes/backup.ts +++ b/server/src/routes/backup.ts @@ -1,5 +1,4 @@ import express, { Request, Response, NextFunction } from 'express'; -import { StringParams } from '../types'; import archiver from 'archiver'; import unzipper from 'unzipper'; import multer from 'multer'; @@ -18,7 +17,7 @@ const MAX_BACKUP_UPLOAD_SIZE = 500 * 1024 * 1024; // 500 MB const backupAttempts = new Map(); function backupRateLimiter(maxAttempts: number, windowMs: number) { - return (req: Request, res: Response, next: NextFunction) => { + return (req: Request, res: Response, next: NextFunction) => { const key = req.ip || 'unknown'; const now = Date.now(); const record = backupAttempts.get(key); @@ -120,7 +119,7 @@ router.post('/create', backupRateLimiter(3, BACKUP_RATE_WINDOW), async (_req: Re } }); -router.get('/download/:filename', (req: Request, res: Response) => { +router.get('/download/:filename', (req: Request, res: Response) => { const { filename } = req.params; if (!/^backup-[\w\-]+\.zip$/.test(filename)) { @@ -183,7 +182,7 @@ async function restoreFromZip(zipPath: string, res: Response) { } } -router.post('/restore/:filename', async (req: Request, res: Response) => { +router.post('/restore/:filename', async (req: Request, res: Response) => { const { filename } = req.params; if (!/^backup-[\w\-]+\.zip$/.test(filename)) { return res.status(400).json({ error: 'Invalid filename' }); @@ -204,7 +203,7 @@ const uploadTmp = multer({ limits: { fileSize: MAX_BACKUP_UPLOAD_SIZE }, }); -router.post('/upload-restore', uploadTmp.single('backup'), async (req: Request, res: Response) => { +router.post('/upload-restore', uploadTmp.single('backup'), async (req: Request, res: Response) => { if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); const zipPath = req.file.path; await restoreFromZip(zipPath, res); @@ -244,7 +243,7 @@ function parseAutoBackupBody(body: Record): { return { enabled, interval, keep_days }; } -router.put('/auto-settings', (req: Request, res: Response) => { +router.put('/auto-settings', (req: Request, res: Response) => { try { const settings = parseAutoBackupBody((req.body || {}) as Record); scheduler.saveSettings(settings); @@ -260,7 +259,7 @@ router.put('/auto-settings', (req: Request, res: Response) => { } }); -router.delete('/:filename', (req: Request, res: Response) => { +router.delete('/:filename', (req: Request, res: Response) => { const { filename } = req.params; if (!/^backup-[\w\-]+\.zip$/.test(filename)) { diff --git a/server/src/routes/budget.ts b/server/src/routes/budget.ts index c371181..c1b9258 100644 --- a/server/src/routes/budget.ts +++ b/server/src/routes/budget.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import { db, canAccessTrip } from '../db/database'; import { authenticate } from '../middleware/auth'; import { broadcast } from '../websocket'; -import { StringParams, AuthRequest, BudgetItem, BudgetItemMember } from '../types'; +import { AuthRequest, BudgetItem, BudgetItemMember } from '../types'; const router = express.Router({ mergeParams: true }); @@ -23,7 +23,7 @@ function avatarUrl(user: { avatar?: string | null }): string | null { return user.avatar ? `/uploads/avatars/${user.avatar}` : null; } -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; @@ -55,7 +55,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { res.json({ items }); }); -router.get('/summary/per-person', authenticate, (req: Request, res: Response) => { +router.get('/summary/per-person', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; if (!canAccessTrip(Number(tripId), authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -75,7 +75,7 @@ router.get('/summary/per-person', authenticate, (req: Request, res res.json({ summary: summary.map(s => ({ ...s, avatar_url: avatarUrl(s) })) }); }); -router.post('/', authenticate, (req: Request, res: Response) => { +router.post('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { category, name, total_price, persons, days, note } = req.body; @@ -107,7 +107,7 @@ router.post('/', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'budget:created', { item }, req.headers['x-socket-id'] as string); }); -router.put('/:id', authenticate, (req: Request, res: Response) => { +router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { category, name, total_price, persons, days, note, sort_order } = req.body; @@ -145,7 +145,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => broadcast(tripId, 'budget:updated', { item: updated }, req.headers['x-socket-id'] as string); }); -router.put('/:id/members', authenticate, (req: Request, res: Response) => { +router.put('/:id/members', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!canAccessTrip(Number(tripId), authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -175,7 +175,7 @@ router.put('/:id/members', authenticate, (req: Request, res: Respo broadcast(Number(tripId), 'budget:members-updated', { itemId: Number(id), members, persons: (updated as BudgetItem).persons }, req.headers['x-socket-id'] as string); }); -router.put('/:id/members/:userId/paid', authenticate, (req: Request, res: Response) => { +router.put('/:id/members/:userId/paid', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id, userId } = req.params; if (!canAccessTrip(Number(tripId), authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -195,7 +195,7 @@ router.put('/:id/members/:userId/paid', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; diff --git a/server/src/routes/collab.ts b/server/src/routes/collab.ts index 432d2ed..3c5d6b2 100644 --- a/server/src/routes/collab.ts +++ b/server/src/routes/collab.ts @@ -7,7 +7,7 @@ import { db, canAccessTrip } from '../db/database'; import { authenticate } from '../middleware/auth'; import { broadcast } from '../websocket'; import { validateStringLengths } from '../middleware/validate'; -import { StringParams, AuthRequest, CollabNote, CollabPoll, CollabMessage, TripFile } from '../types'; +import { AuthRequest, CollabNote, CollabPoll, CollabMessage, TripFile } from '../types'; interface ReactionRow { emoji: string; @@ -90,7 +90,7 @@ function formatMessage(msg: CollabMessage, reactions?: { emoji: string; users: { return { ...msg, user_avatar: avatarUrl(msg), avatar_url: avatarUrl(msg), reactions: reactions || [] }; } -router.get('/notes', authenticate, (req: Request, res: Response) => { +router.get('/notes', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -106,7 +106,7 @@ router.get('/notes', authenticate, (req: Request, res: Response) = res.json({ notes: notes.map(formatNote) }); }); -router.post('/notes', authenticate, (req: Request, res: Response) => { +router.post('/notes', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { title, content, category, color, website } = req.body; @@ -127,7 +127,7 @@ router.post('/notes', authenticate, (req: Request, res: Response) broadcast(tripId, 'collab:note:created', { note: formatted }, req.headers['x-socket-id'] as string); }); -router.put('/notes/:id', authenticate, (req: Request, res: Response) => { +router.put('/notes/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { title, content, category, color, pinned, website } = req.body; @@ -165,7 +165,7 @@ router.put('/notes/:id', authenticate, (req: Request, res: Respons broadcast(tripId, 'collab:note:updated', { note: formatted }, req.headers['x-socket-id'] as string); }); -router.delete('/notes/:id', authenticate, (req: Request, res: Response) => { +router.delete('/notes/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -185,7 +185,7 @@ router.delete('/notes/:id', authenticate, (req: Request, res: Resp broadcast(tripId, 'collab:note:deleted', { noteId: Number(id) }, req.headers['x-socket-id'] as string); }); -router.post('/notes/:id/files', authenticate, noteUpload.single('file'), (req: Request, res: Response) => { +router.post('/notes/:id/files', authenticate, noteUpload.single('file'), (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!verifyTripAccess(Number(tripId), authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -203,7 +203,7 @@ router.post('/notes/:id/files', authenticate, noteUpload.single('file'), (req: R broadcast(Number(tripId), 'collab:note:updated', { note: formatNote(db.prepare('SELECT n.*, u.username, u.avatar FROM collab_notes n JOIN users u ON n.user_id = u.id WHERE n.id = ?').get(id) as CollabNote) }, req.headers['x-socket-id'] as string); }); -router.delete('/notes/:id/files/:fileId', authenticate, (req: Request, res: Response) => { +router.delete('/notes/:id/files/:fileId', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id, fileId } = req.params; if (!verifyTripAccess(Number(tripId), authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -254,7 +254,7 @@ function getPollWithVotes(pollId: number | bigint | string) { }; } -router.get('/polls', authenticate, (req: Request, res: Response) => { +router.get('/polls', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -267,7 +267,7 @@ router.get('/polls', authenticate, (req: Request, res: Response) = res.json({ polls }); }); -router.post('/polls', authenticate, (req: Request, res: Response) => { +router.post('/polls', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { question, options, multiple, multiple_choice, deadline } = req.body; @@ -289,7 +289,7 @@ router.post('/polls', authenticate, (req: Request, res: Response) broadcast(tripId, 'collab:poll:created', { poll }, req.headers['x-socket-id'] as string); }); -router.post('/polls/:id/vote', authenticate, (req: Request, res: Response) => { +router.post('/polls/:id/vote', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { option_index } = req.body; @@ -322,7 +322,7 @@ router.post('/polls/:id/vote', authenticate, (req: Request, res: R broadcast(tripId, 'collab:poll:voted', { poll: updatedPoll }, req.headers['x-socket-id'] as string); }); -router.put('/polls/:id/close', authenticate, (req: Request, res: Response) => { +router.put('/polls/:id/close', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -337,7 +337,7 @@ router.put('/polls/:id/close', authenticate, (req: Request, res: R broadcast(tripId, 'collab:poll:closed', { poll: updatedPoll }, req.headers['x-socket-id'] as string); }); -router.delete('/polls/:id', authenticate, (req: Request, res: Response) => { +router.delete('/polls/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -350,7 +350,7 @@ router.delete('/polls/:id', authenticate, (req: Request, res: Resp broadcast(tripId, 'collab:poll:deleted', { pollId: Number(id) }, req.headers['x-socket-id'] as string); }); -router.get('/messages', authenticate, (req: Request, res: Response) => { +router.get('/messages', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { before } = req.query; @@ -390,7 +390,7 @@ router.get('/messages', authenticate, (req: Request, res: Response res.json({ messages: messages.map(m => formatMessage(m, groupReactions(reactionsByMsg[m.id] || []))) }); }); -router.post('/messages', authenticate, validateStringLengths({ text: 5000 }), (req: Request, res: Response) => { +router.post('/messages', authenticate, validateStringLengths({ text: 5000 }), (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { text, reply_to } = req.body; @@ -421,7 +421,7 @@ router.post('/messages', authenticate, validateStringLengths({ text: 5000 }), (r broadcast(tripId, 'collab:message:created', { message: formatted }, req.headers['x-socket-id'] as string); }); -router.post('/messages/:id/react', authenticate, (req: Request, res: Response) => { +router.post('/messages/:id/react', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { emoji } = req.body; @@ -443,7 +443,7 @@ router.post('/messages/:id/react', authenticate, (req: Request, re broadcast(Number(tripId), 'collab:message:reacted', { messageId: Number(id), reactions }, req.headers['x-socket-id'] as string); }); -router.delete('/messages/:id', authenticate, (req: Request, res: Response) => { +router.delete('/messages/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; if (!verifyTripAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -457,7 +457,7 @@ router.delete('/messages/:id', authenticate, (req: Request, res: R broadcast(tripId, 'collab:message:deleted', { messageId: Number(id), username: message.username || authReq.user.username }, req.headers['x-socket-id'] as string); }); -router.get('/link-preview', authenticate, async (req: Request, res: Response) => { +router.get('/link-preview', authenticate, async (req: Request, res: Response) => { const { url } = req.query as { url?: string }; if (!url) return res.status(400).json({ error: 'URL is required' }); diff --git a/server/src/routes/dayNotes.ts b/server/src/routes/dayNotes.ts index ce2abd5..1605c8e 100644 --- a/server/src/routes/dayNotes.ts +++ b/server/src/routes/dayNotes.ts @@ -3,7 +3,7 @@ import { db, canAccessTrip } from '../db/database'; import { authenticate } from '../middleware/auth'; import { broadcast } from '../websocket'; import { validateStringLengths } from '../middleware/validate'; -import { StringParams, AuthRequest, DayNote } from '../types'; +import { AuthRequest, DayNote } from '../types'; const router = express.Router({ mergeParams: true }); @@ -11,7 +11,7 @@ function verifyAccess(tripId: string | number, userId: number) { return canAccessTrip(tripId, userId); } -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, dayId } = req.params; if (!verifyAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -23,7 +23,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { res.json({ notes }); }); -router.post('/', authenticate, validateStringLengths({ text: 500, time: 150 }), (req: Request, res: Response) => { +router.post('/', authenticate, validateStringLengths({ text: 500, time: 150 }), (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, dayId } = req.params; if (!verifyAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -43,7 +43,7 @@ router.post('/', authenticate, validateStringLengths({ text: 500, time: 150 }), broadcast(tripId, 'dayNote:created', { dayId: Number(dayId), note }, req.headers['x-socket-id'] as string); }); -router.put('/:id', authenticate, validateStringLengths({ text: 500, time: 150 }), (req: Request, res: Response) => { +router.put('/:id', authenticate, validateStringLengths({ text: 500, time: 150 }), (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, dayId, id } = req.params; if (!verifyAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -67,7 +67,7 @@ router.put('/:id', authenticate, validateStringLengths({ text: 500, time: 150 }) broadcast(tripId, 'dayNote:updated', { dayId: Number(dayId), note: updated }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, dayId, id } = req.params; if (!verifyAccess(tripId, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); diff --git a/server/src/routes/days.ts b/server/src/routes/days.ts index de69f5c..c59e1b7 100644 --- a/server/src/routes/days.ts +++ b/server/src/routes/days.ts @@ -4,7 +4,7 @@ import { authenticate } from '../middleware/auth'; import { requireTripAccess } from '../middleware/tripAccess'; import { broadcast } from '../websocket'; import { loadTagsByPlaceIds, loadParticipantsByAssignmentIds, formatAssignmentWithPlace } from '../services/queryHelpers'; -import { StringParams, AuthRequest, AssignmentRow, Day, DayNote } from '../types'; +import { AuthRequest, AssignmentRow, Day, DayNote } from '../types'; const router = express.Router({ mergeParams: true }); @@ -68,7 +68,7 @@ function getAssignmentsForDay(dayId: number | string) { }); } -router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId } = req.params; const days = db.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(tripId) as Day[]; @@ -125,7 +125,7 @@ router.get('/', authenticate, requireTripAccess, (req: Request, re res.json({ days: daysWithAssignments }); }); -router.post('/', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.post('/', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId } = req.params; const { date, notes } = req.body; @@ -143,7 +143,7 @@ router.post('/', authenticate, requireTripAccess, (req: Request, r broadcast(tripId, 'day:created', { day: dayResult }, req.headers['x-socket-id'] as string); }); -router.put('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.put('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const day = db.prepare('SELECT * FROM days WHERE id = ? AND trip_id = ?').get(id, tripId) as Day | undefined; @@ -160,7 +160,7 @@ router.put('/:id', authenticate, requireTripAccess, (req: Request, broadcast(tripId, 'day:updated', { day: dayWithAssignments }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const day = db.prepare('SELECT * FROM days WHERE id = ? AND trip_id = ?').get(id, tripId); @@ -184,7 +184,7 @@ function getAccommodationWithPlace(id: number | bigint) { `).get(id); } -accommodationsRouter.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { +accommodationsRouter.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId } = req.params; const accommodations = db.prepare(` @@ -198,7 +198,7 @@ accommodationsRouter.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { +accommodationsRouter.post('/', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId } = req.params; const { place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes } = req.body; @@ -242,7 +242,7 @@ accommodationsRouter.post('/', authenticate, requireTripAccess, (req: Request, res: Response) => { +accommodationsRouter.put('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; interface DayAccommodation { id: number; trip_id: number; place_id: number; start_day_id: number; end_day_id: number; check_in: string | null; check_out: string | null; confirmation: string | null; notes: string | null; } @@ -293,7 +293,7 @@ accommodationsRouter.put('/:id', authenticate, requireTripAccess, (req: Request< broadcast(tripId, 'accommodation:updated', { accommodation }, req.headers['x-socket-id'] as string); }); -accommodationsRouter.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +accommodationsRouter.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params; const existing = db.prepare('SELECT * FROM day_accommodations WHERE id = ? AND trip_id = ?').get(id, tripId); diff --git a/server/src/routes/files.ts b/server/src/routes/files.ts index 7fa48db..378f074 100644 --- a/server/src/routes/files.ts +++ b/server/src/routes/files.ts @@ -7,7 +7,7 @@ import { db, canAccessTrip } from '../db/database'; import { authenticate, demoUploadBlock } from '../middleware/auth'; import { requireTripAccess } from '../middleware/tripAccess'; import { broadcast } from '../websocket'; -import { StringParams, AuthRequest, TripFile } from '../types'; +import { AuthRequest, TripFile } from '../types'; const router = express.Router({ mergeParams: true }); @@ -72,7 +72,7 @@ function formatFile(file: TripFile) { } // List files (excludes soft-deleted by default) -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const showTrash = req.query.trash === 'true'; @@ -86,7 +86,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { }); // Upload file -router.post('/', authenticate, requireTripAccess, demoUploadBlock, upload.single('file'), (req: Request, res: Response) => { +router.post('/', authenticate, requireTripAccess, demoUploadBlock, upload.single('file'), (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { place_id, description, reservation_id } = req.body; @@ -116,7 +116,7 @@ router.post('/', authenticate, requireTripAccess, demoUploadBlock, upload.single }); // Update file metadata -router.put('/:id', authenticate, (req: Request, res: Response) => { +router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { description, place_id, reservation_id } = req.body; @@ -146,7 +146,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => }); // Toggle starred -router.patch('/:id/star', authenticate, (req: Request, res: Response) => { +router.patch('/:id/star', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; @@ -165,7 +165,7 @@ router.patch('/:id/star', authenticate, (req: Request, res: Respon }); // Soft-delete (move to trash) -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; @@ -181,7 +181,7 @@ router.delete('/:id', authenticate, (req: Request, res: Response) }); // Restore from trash -router.post('/:id/restore', authenticate, (req: Request, res: Response) => { +router.post('/:id/restore', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; @@ -199,7 +199,7 @@ router.post('/:id/restore', authenticate, (req: Request, res: Resp }); // Permanently delete from trash -router.delete('/:id/permanent', authenticate, (req: Request, res: Response) => { +router.delete('/:id/permanent', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; @@ -220,7 +220,7 @@ router.delete('/:id/permanent', authenticate, (req: Request, res: }); // Empty entire trash -router.delete('/trash/empty', authenticate, (req: Request, res: Response) => { +router.delete('/trash/empty', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; diff --git a/server/src/routes/maps.ts b/server/src/routes/maps.ts index 97388d7..b71e80d 100644 --- a/server/src/routes/maps.ts +++ b/server/src/routes/maps.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import fetch from 'node-fetch'; import { db } from '../db/database'; import { authenticate } from '../middleware/auth'; -import { StringParams, AuthRequest } from '../types'; +import { AuthRequest } from '../types'; interface NominatimResult { osm_type: string; @@ -247,7 +247,7 @@ async function searchNominatim(query: string, lang?: string) { })); } -router.post('/search', authenticate, async (req: Request, res: Response) => { +router.post('/search', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { query } = req.body; @@ -301,7 +301,7 @@ router.post('/search', authenticate, async (req: Request, res: Res } }); -router.get('/details/:placeId', authenticate, async (req: Request, res: Response) => { +router.get('/details/:placeId', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { placeId } = req.params; @@ -372,7 +372,7 @@ router.get('/details/:placeId', authenticate, async (req: Request, } }); -router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Response) => { +router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { placeId } = req.params; @@ -453,7 +453,7 @@ router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Response) => { +router.get('/reverse', authenticate, async (req: Request, res: Response) => { const { lat, lng, lang } = req.query as { lat: string; lng: string; lang?: string }; if (!lat || !lng) return res.status(400).json({ error: 'lat and lng required' }); try { diff --git a/server/src/routes/packing.ts b/server/src/routes/packing.ts index fac3e39..22cb7b7 100644 --- a/server/src/routes/packing.ts +++ b/server/src/routes/packing.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import { db, canAccessTrip } from '../db/database'; import { authenticate } from '../middleware/auth'; import { broadcast } from '../websocket'; -import { StringParams, AuthRequest } from '../types'; +import { AuthRequest } from '../types'; const router = express.Router({ mergeParams: true }); @@ -10,7 +10,7 @@ function verifyTripOwnership(tripId: string | number, userId: number) { return canAccessTrip(tripId, userId); } -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; @@ -24,7 +24,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { res.json({ items }); }); -router.post('/', authenticate, (req: Request, res: Response) => { +router.post('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { name, category, checked } = req.body; @@ -46,7 +46,7 @@ router.post('/', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'packing:created', { item }, req.headers['x-socket-id'] as string); }); -router.put('/:id', authenticate, (req: Request, res: Response) => { +router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { name, checked, category } = req.body; @@ -76,7 +76,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => broadcast(tripId, 'packing:updated', { item: updated }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; @@ -91,7 +91,7 @@ router.delete('/:id', authenticate, (req: Request, res: Response) broadcast(tripId, 'packing:deleted', { itemId: Number(id) }, req.headers['x-socket-id'] as string); }); -router.put('/reorder', authenticate, (req: Request, res: Response) => { +router.put('/reorder', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { orderedIds } = req.body; diff --git a/server/src/routes/places.ts b/server/src/routes/places.ts index d2704ea..176aa3c 100644 --- a/server/src/routes/places.ts +++ b/server/src/routes/places.ts @@ -6,7 +6,7 @@ import { requireTripAccess } from '../middleware/tripAccess'; import { broadcast } from '../websocket'; import { loadTagsByPlaceIds } from '../services/queryHelpers'; import { validateStringLengths } from '../middleware/validate'; -import { StringParams, AuthRequest, Place } from '../types'; +import { AuthRequest, Place } from '../types'; interface PlaceWithCategory extends Place { category_name: string | null; @@ -21,7 +21,7 @@ interface UnsplashSearchResponse { const router = express.Router({ mergeParams: true }); -router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.get('/', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId } = req.params const { search, category, tag } = req.query; @@ -72,7 +72,7 @@ router.get('/', authenticate, requireTripAccess, (req: Request, re res.json({ places: placesWithTags }); }); -router.post('/', authenticate, requireTripAccess, validateStringLengths({ name: 200, description: 2000, address: 500, notes: 2000 }), (req: Request, res: Response) => { +router.post('/', authenticate, requireTripAccess, validateStringLengths({ name: 200, description: 2000, address: 500, notes: 2000 }), (req: Request, res: Response) => { const { tripId } = req.params const { @@ -112,7 +112,7 @@ router.post('/', authenticate, requireTripAccess, validateStringLengths({ name: broadcast(tripId, 'place:created', { place }, req.headers['x-socket-id'] as string); }); -router.get('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.get('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params const placeCheck = db.prepare('SELECT id FROM places WHERE id = ? AND trip_id = ?').get(id, tripId); @@ -124,7 +124,7 @@ router.get('/:id', authenticate, requireTripAccess, (req: Request, res.json({ place }); }); -router.get('/:id/image', authenticate, requireTripAccess, async (req: Request, res: Response) => { +router.get('/:id/image', authenticate, requireTripAccess, async (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params @@ -165,7 +165,7 @@ router.get('/:id/image', authenticate, requireTripAccess, async (req: Request, res: Response) => { +router.put('/:id', authenticate, requireTripAccess, validateStringLengths({ name: 200, description: 2000, address: 500, notes: 2000 }), (req: Request, res: Response) => { const { tripId, id } = req.params const existingPlace = db.prepare('SELECT * FROM places WHERE id = ? AND trip_id = ?').get(id, tripId) as Place | undefined; @@ -237,7 +237,7 @@ router.put('/:id', authenticate, requireTripAccess, validateStringLengths({ name broadcast(tripId, 'place:updated', { place }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { +router.delete('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => { const { tripId, id } = req.params const place = db.prepare('SELECT id FROM places WHERE id = ? AND trip_id = ?').get(id, tripId); diff --git a/server/src/routes/reservations.ts b/server/src/routes/reservations.ts index 315e39c..302c1a1 100644 --- a/server/src/routes/reservations.ts +++ b/server/src/routes/reservations.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import { db, canAccessTrip } from '../db/database'; import { authenticate } from '../middleware/auth'; import { broadcast } from '../websocket'; -import { StringParams, AuthRequest, Reservation } from '../types'; +import { AuthRequest, Reservation } from '../types'; const router = express.Router({ mergeParams: true }); @@ -10,7 +10,7 @@ function verifyTripOwnership(tripId: string | number, userId: number) { return canAccessTrip(tripId, userId); } -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; @@ -32,7 +32,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { res.json({ reservations }); }); -router.post('/', authenticate, (req: Request, res: Response) => { +router.post('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type, accommodation_id, metadata, create_accommodation } = req.body; @@ -103,7 +103,7 @@ router.post('/', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'reservation:created', { reservation }, req.headers['x-socket-id'] as string); }); -router.put('/:id', authenticate, (req: Request, res: Response) => { +router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type, accommodation_id, metadata, create_accommodation } = req.body; @@ -195,7 +195,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => broadcast(tripId, 'reservation:updated', { reservation: updated }, req.headers['x-socket-id'] as string); }); -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; diff --git a/server/src/routes/trips.ts b/server/src/routes/trips.ts index 05ffea9..ad5231d 100644 --- a/server/src/routes/trips.ts +++ b/server/src/routes/trips.ts @@ -6,7 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { db, canAccessTrip, isOwner } from '../db/database'; import { authenticate, demoUploadBlock } from '../middleware/auth'; import { broadcast } from '../websocket'; -import { StringParams, AuthRequest, Trip, User } from '../types'; +import { AuthRequest, Trip, User } from '../types'; const router = express.Router(); @@ -120,7 +120,7 @@ function generateDays(tripId: number | bigint | string, startDate: string | null } } -router.get('/', authenticate, (req: Request, res: Response) => { +router.get('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const archived = req.query.archived === '1' ? 1 : 0; const userId = authReq.user.id; @@ -133,7 +133,7 @@ router.get('/', authenticate, (req: Request, res: Response) => { res.json({ trips }); }); -router.post('/', authenticate, (req: Request, res: Response) => { +router.post('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { title, description, start_date, end_date, currency } = req.body; if (!title) return res.status(400).json({ error: 'Title is required' }); @@ -151,7 +151,7 @@ router.post('/', authenticate, (req: Request, res: Response) => { res.status(201).json({ trip }); }); -router.get('/:id', authenticate, (req: Request, res: Response) => { +router.get('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const userId = authReq.user.id; const trip = db.prepare(` @@ -163,7 +163,7 @@ router.get('/:id', authenticate, (req: Request, res: Response) => res.json({ trip }); }); -router.put('/:id', authenticate, (req: Request, res: Response) => { +router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const access = canAccessTrip(req.params.id, authReq.user.id); if (!access) return res.status(404).json({ error: 'Trip not found' }); @@ -201,7 +201,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => broadcast(req.params.id, 'trip:updated', { trip: updatedTrip }, req.headers['x-socket-id'] as string); }); -router.post('/:id/cover', authenticate, demoUploadBlock, uploadCover.single('cover'), (req: Request, res: Response) => { +router.post('/:id/cover', authenticate, demoUploadBlock, uploadCover.single('cover'), (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!isOwner(req.params.id, authReq.user.id)) return res.status(403).json({ error: 'Only the owner can change the cover image' }); @@ -224,7 +224,7 @@ router.post('/:id/cover', authenticate, demoUploadBlock, uploadCover.single('cov res.json({ cover_image: coverUrl }); }); -router.delete('/:id', authenticate, (req: Request, res: Response) => { +router.delete('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!isOwner(req.params.id, authReq.user.id)) return res.status(403).json({ error: 'Only the owner can delete the trip' }); @@ -234,7 +234,7 @@ router.delete('/:id', authenticate, (req: Request, res: Response) broadcast(deletedTripId, 'trip:deleted', { id: deletedTripId }, req.headers['x-socket-id'] as string); }); -router.get('/:id/members', authenticate, (req: Request, res: Response) => { +router.get('/:id/members', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!canAccessTrip(req.params.id, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -261,7 +261,7 @@ router.get('/:id/members', authenticate, (req: Request, res: Respo }); }); -router.post('/:id/members', authenticate, (req: Request, res: Response) => { +router.post('/:id/members', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!canAccessTrip(req.params.id, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); @@ -287,7 +287,7 @@ router.post('/:id/members', authenticate, (req: Request, res: Resp res.status(201).json({ member: { ...target, role: 'member', avatar_url: target.avatar ? `/uploads/avatars/${target.avatar}` : null } }); }); -router.delete('/:id/members/:userId', authenticate, (req: Request, res: Response) => { +router.delete('/:id/members/:userId', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!canAccessTrip(req.params.id, authReq.user.id)) return res.status(404).json({ error: 'Trip not found' }); diff --git a/server/src/routes/vacay.ts b/server/src/routes/vacay.ts index a645cdb..02b13d9 100644 --- a/server/src/routes/vacay.ts +++ b/server/src/routes/vacay.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from 'express'; import { db } from '../db/database'; import { authenticate } from '../middleware/auth'; -import { StringParams, AuthRequest } from '../types'; +import { AuthRequest } from '../types'; interface VacayPlan { id: number; @@ -100,7 +100,7 @@ function getPlanUsers(planId: number) { return [owner, ...members]; } -router.get('/plan', (req: Request, res: Response) => { +router.get('/plan', (req: Request, res: Response) => { const authReq = req as AuthRequest; const plan = getActivePlan(authReq.user.id); const activePlanId = plan.id; @@ -140,7 +140,7 @@ router.get('/plan', (req: Request, res: Response) => { }); }); -router.put('/plan', async (req: Request, res: Response) => { +router.put('/plan', async (req: Request, res: Response) => { const authReq = req as AuthRequest; const planId = getActivePlanId(authReq.user.id); const { block_weekends, holidays_enabled, holidays_region, company_holidays_enabled, carry_over_enabled } = req.body; @@ -222,7 +222,7 @@ router.put('/plan', async (req: Request, res: Response) => { }); }); -router.put('/color', (req: Request, res: Response) => { +router.put('/color', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { color, target_user_id } = req.body; const planId = getActivePlanId(authReq.user.id); @@ -239,7 +239,7 @@ router.put('/color', (req: Request, res: Response) => { res.json({ success: true }); }); -router.post('/invite', (req: Request, res: Response) => { +router.post('/invite', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { user_id } = req.body; if (!user_id) return res.status(400).json({ error: 'user_id required' }); @@ -273,7 +273,7 @@ router.post('/invite', (req: Request, res: Response) => { res.json({ success: true }); }); -router.post('/invite/accept', (req: Request, res: Response) => { +router.post('/invite/accept', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { plan_id } = req.body; const invite = db.prepare("SELECT * FROM vacay_plan_members WHERE plan_id = ? AND user_id = ? AND status = 'pending'").get(plan_id, authReq.user.id) as VacayPlanMember | undefined; @@ -318,7 +318,7 @@ router.post('/invite/accept', (req: Request, res: Response) => { res.json({ success: true }); }); -router.post('/invite/decline', (req: Request, res: Response) => { +router.post('/invite/decline', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { plan_id } = req.body; db.prepare("DELETE FROM vacay_plan_members WHERE plan_id = ? AND user_id = ? AND status = 'pending'").run(plan_id, authReq.user.id); @@ -326,7 +326,7 @@ router.post('/invite/decline', (req: Request, res: Response) => { res.json({ success: true }); }); -router.post('/invite/cancel', (req: Request, res: Response) => { +router.post('/invite/cancel', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { user_id } = req.body; const plan = getActivePlan(authReq.user.id); @@ -340,7 +340,7 @@ router.post('/invite/cancel', (req: Request, res: Response) => { res.json({ success: true }); }); -router.post('/dissolve', (req: Request, res: Response) => { +router.post('/dissolve', (req: Request, res: Response) => { const authReq = req as AuthRequest; const plan = getActivePlan(authReq.user.id); const isOwnerFlag = plan.owner_id === authReq.user.id; @@ -375,7 +375,7 @@ router.post('/dissolve', (req: Request, res: Response) => { res.json({ success: true }); }); -router.get('/available-users', (req: Request, res: Response) => { +router.get('/available-users', (req: Request, res: Response) => { const authReq = req as AuthRequest; const planId = getActivePlanId(authReq.user.id); const users = db.prepare(` @@ -391,14 +391,14 @@ router.get('/available-users', (req: Request, res: Response) => { res.json({ users }); }); -router.get('/years', (req: Request, res: Response) => { +router.get('/years', (req: Request, res: Response) => { const authReq = req as AuthRequest; const planId = getActivePlanId(authReq.user.id); const years = db.prepare('SELECT year FROM vacay_years WHERE plan_id = ? ORDER BY year').all(planId) as { year: number }[]; res.json({ years: years.map(y => y.year) }); }); -router.post('/years', (req: Request, res: Response) => { +router.post('/years', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { year } = req.body; if (!year) return res.status(400).json({ error: 'Year required' }); @@ -426,7 +426,7 @@ router.post('/years', (req: Request, res: Response) => { res.json({ years: years.map(y => y.year) }); }); -router.delete('/years/:year', (req: Request, res: Response) => { +router.delete('/years/:year', (req: Request, res: Response) => { const authReq = req as AuthRequest; const year = parseInt(req.params.year); const planId = getActivePlanId(authReq.user.id); @@ -438,7 +438,7 @@ router.delete('/years/:year', (req: Request, res: Response) => { res.json({ years: years.map(y => y.year) }); }); -router.get('/entries/:year', (req: Request, res: Response) => { +router.get('/entries/:year', (req: Request, res: Response) => { const authReq = req as AuthRequest; const year = req.params.year; const planId = getActivePlanId(authReq.user.id); @@ -453,7 +453,7 @@ router.get('/entries/:year', (req: Request, res: Response) => { res.json({ entries, companyHolidays }); }); -router.post('/entries/toggle', (req: Request, res: Response) => { +router.post('/entries/toggle', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { date, target_user_id } = req.body; if (!date) return res.status(400).json({ error: 'date required' }); @@ -479,7 +479,7 @@ router.post('/entries/toggle', (req: Request, res: Response) => { } }); -router.post('/entries/company-holiday', (req: Request, res: Response) => { +router.post('/entries/company-holiday', (req: Request, res: Response) => { const authReq = req as AuthRequest; const { date, note } = req.body; const planId = getActivePlanId(authReq.user.id); @@ -496,7 +496,7 @@ router.post('/entries/company-holiday', (req: Request, res: Respon } }); -router.get('/stats/:year', (req: Request, res: Response) => { +router.get('/stats/:year', (req: Request, res: Response) => { const authReq = req as AuthRequest; const year = parseInt(req.params.year); const planId = getActivePlanId(authReq.user.id); @@ -532,7 +532,7 @@ router.get('/stats/:year', (req: Request, res: Response) => { res.json({ stats }); }); -router.put('/stats/:year', (req: Request, res: Response) => { +router.put('/stats/:year', (req: Request, res: Response) => { const authReq = req as AuthRequest; const year = parseInt(req.params.year); const { vacation_days, target_user_id } = req.body; @@ -564,7 +564,7 @@ router.get('/holidays/countries', async (_req: Request, res: Response) => { } }); -router.get('/holidays/:year/:country', async (req: Request, res: Response) => { +router.get('/holidays/:year/:country', async (req: Request, res: Response) => { const { year, country } = req.params; const cacheKey = `${year}-${country}`; const cached = holidayCache.get(cacheKey); diff --git a/server/src/types.ts b/server/src/types.ts index 190ce0a..b9f2142 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,9 +1,5 @@ import { Request } from 'express'; -// Route params from Express 4 are always strings at runtime. -// Use this with Request to get string-typed req.params. -export type StringParams = Record; - export interface User { id: number; username: string;