chore: apply prettier on the entire project

This commit is contained in:
jubnl
2026-05-25 21:59:42 +02:00
parent c130ed41be
commit 6bcdfbc34b
488 changed files with 82986 additions and 45830 deletions
+199 -153
View File
@@ -1,60 +1,59 @@
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import cookieParser from 'cookie-parser';
import path from 'node:path';
import fs from 'node:fs';
import multer from 'multer';
import { logDebug, logWarn, logError } from './services/auditLog';
import { enforceGlobalMfaPolicy } from './middleware/mfaPolicy';
import { authenticate, verifyJwtAndLoadUser } from './middleware/auth';
import { ADDON_IDS } from './addons';
import { db } from './db/database';
import authRoutes from './routes/auth';
import tripsRoutes from './routes/trips';
import daysRoutes, { accommodationsRouter as accommodationsRoutes } from './routes/days';
import placesRoutes from './routes/places';
import assignmentsRoutes from './routes/assignments';
import packingRoutes from './routes/packing';
import todoRoutes from './routes/todo';
import tagsRoutes from './routes/tags';
import categoriesRoutes from './routes/categories';
import adminRoutes from './routes/admin';
import mapsRoutes from './routes/maps';
import airportsRoutes from './routes/airports';
import filesRoutes from './routes/files';
import reservationsRoutes from './routes/reservations';
import dayNotesRoutes from './routes/dayNotes';
import settingsRoutes from './routes/settings';
import budgetRoutes from './routes/budget';
import collabRoutes from './routes/collab';
import backupRoutes from './routes/backup';
import oidcRoutes from './routes/oidc';
import { oauthPublicRouter, oauthApiRouter } from './routes/oauth';
import vacayRoutes from './routes/vacay';
import atlasRoutes from './routes/atlas';
import memoriesRoutes from './routes/memories/unified';
import photoRoutes from './routes/photos';
import notificationRoutes from './routes/notifications';
import shareRoutes from './routes/share';
import journeyRoutes from './routes/journey';
import journeyPublicRoutes from './routes/journeyPublic';
import publicConfigRoutes from './routes/publicConfig';
import systemNoticesRoutes from './routes/systemNotices';
import { mcpHandler } from './mcp';
import { trekOAuthProvider, trekClientsStore } from './mcp/oauthProvider';
import { Addon } from './types';
import { getPhotoProviderConfig } from './services/memories/helpersService';
import { ALL_SCOPES } from './mcp/scopes';
import { authenticate, verifyJwtAndLoadUser } from './middleware/auth';
import { enforceGlobalMfaPolicy } from './middleware/mfaPolicy';
import adminRoutes from './routes/admin';
import airportsRoutes from './routes/airports';
import assignmentsRoutes from './routes/assignments';
import atlasRoutes from './routes/atlas';
import authRoutes from './routes/auth';
import backupRoutes from './routes/backup';
import budgetRoutes from './routes/budget';
import categoriesRoutes from './routes/categories';
import collabRoutes from './routes/collab';
import dayNotesRoutes from './routes/dayNotes';
import daysRoutes, { accommodationsRouter as accommodationsRoutes } from './routes/days';
import filesRoutes from './routes/files';
import journeyRoutes from './routes/journey';
import journeyPublicRoutes from './routes/journeyPublic';
import mapsRoutes from './routes/maps';
import memoriesRoutes from './routes/memories/unified';
import notificationRoutes from './routes/notifications';
import { oauthPublicRouter, oauthApiRouter } from './routes/oauth';
import oidcRoutes from './routes/oidc';
import packingRoutes from './routes/packing';
import photoRoutes from './routes/photos';
import placesRoutes from './routes/places';
import publicConfigRoutes from './routes/publicConfig';
import reservationsRoutes from './routes/reservations';
import settingsRoutes from './routes/settings';
import shareRoutes from './routes/share';
import systemNoticesRoutes from './routes/systemNotices';
import tagsRoutes from './routes/tags';
import todoRoutes from './routes/todo';
import tripsRoutes from './routes/trips';
import vacayRoutes from './routes/vacay';
import { getCollabFeatures } from './services/adminService';
import { isAddonEnabled } from './services/adminService';
import { ADDON_IDS } from './addons';
import { ALL_SCOPES } from './mcp/scopes';
import { mcpAuthMetadataRouter } from '@modelcontextprotocol/sdk/server/auth/router';
import { logDebug, logWarn, logError } from './services/auditLog';
import { getPhotoProviderConfig } from './services/memories/helpersService';
import { getMcpSafeUrl } from './services/notifications';
import { Addon } from './types';
import { authorizationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/authorize';
import { clientRegistrationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/register';
import { mcpAuthMetadataRouter } from '@modelcontextprotocol/sdk/server/auth/router';
import type { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth';
import { getMcpSafeUrl } from './services/notifications';
import cookieParser from 'cookie-parser';
import cors from 'cors';
import express, { Request, Response, NextFunction } from 'express';
import helmet from 'helmet';
import multer from 'multer';
import fs from 'node:fs';
import path from 'node:path';
export function createApp(): express.Application {
const app = express();
@@ -65,8 +64,10 @@ export function createApp(): express.Application {
}
const allowedOrigins = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim()).filter(Boolean)
: null;
? process.env.ALLOWED_ORIGINS.split(',')
.map((o) => o.trim())
.filter(Boolean)
: null;
let corsOrigin: cors.CorsOptions['origin'];
if (allowedOrigins) {
@@ -102,54 +103,65 @@ export function createApp(): express.Application {
// answer OPTIONS without Access-Control-Allow-Origin before the SDK's own cors() runs.
// All /.well-known/* paths get open CORS so clients probing openid-configuration or the
// RFC 8414 path-suffixed AS metadata form don't get CORS-blocked (they get 404 JSON instead).
app.use(
(req: Request, _res: Response, next: NextFunction) => {
if (
req.path.startsWith('/.well-known/') ||
req.path === '/oauth/register' ||
req.path === '/oauth/authorize' ||
req.path === '/oauth/userinfo' ||
req.path === '/mcp'
) {
cors({ origin: '*', credentials: false })(req, _res, next);
} else {
next();
}
},
);
app.use((req: Request, _res: Response, next: NextFunction) => {
if (
req.path.startsWith('/.well-known/') ||
req.path === '/oauth/register' ||
req.path === '/oauth/authorize' ||
req.path === '/oauth/userinfo' ||
req.path === '/mcp'
) {
cors({ origin: '*', credentials: false })(req, _res, next);
} else {
next();
}
});
app.use(cors({ origin: corsOrigin, credentials: true }));
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'wasm-unsafe-eval'", "'unsafe-eval'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://unpkg.com"],
imgSrc: ["'self'", "data:", "blob:", "https:"],
connectSrc: [
"'self'", "ws:", "wss:",
"https://nominatim.openstreetmap.org", "https://overpass-api.de",
"https://places.googleapis.com", "https://api.openweathermap.org",
"https://en.wikipedia.org", "https://commons.wikimedia.org",
"https://*.basemaps.cartocdn.com", "https://*.tile.openstreetmap.org",
"https://unpkg.com", "https://open-meteo.com", "https://api.open-meteo.com",
"https://geocoding-api.open-meteo.com", "https://api.exchangerate-api.com",
"https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_countries.geojson",
"https://router.project-osrm.org/route/v1/",
"https://api.mapbox.com", "https://*.tiles.mapbox.com", "https://events.mapbox.com"
],
workerSrc: ["'self'", "blob:"],
childSrc: ["'self'", "blob:"],
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
frameAncestors: ["'self'"],
upgradeInsecureRequests: shouldForceHttps ? [] : null
}
},
crossOriginEmbedderPolicy: false,
hsts: hstsActive ? { maxAge: 31536000, includeSubDomains: hstsIncludeSubdomains } : false,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'wasm-unsafe-eval'", "'unsafe-eval'"],
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com', 'https://unpkg.com'],
imgSrc: ["'self'", 'data:', 'blob:', 'https:'],
connectSrc: [
"'self'",
'ws:',
'wss:',
'https://nominatim.openstreetmap.org',
'https://overpass-api.de',
'https://places.googleapis.com',
'https://api.openweathermap.org',
'https://en.wikipedia.org',
'https://commons.wikimedia.org',
'https://*.basemaps.cartocdn.com',
'https://*.tile.openstreetmap.org',
'https://unpkg.com',
'https://open-meteo.com',
'https://api.open-meteo.com',
'https://geocoding-api.open-meteo.com',
'https://api.exchangerate-api.com',
'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_countries.geojson',
'https://router.project-osrm.org/route/v1/',
'https://api.mapbox.com',
'https://*.tiles.mapbox.com',
'https://events.mapbox.com',
],
workerSrc: ["'self'", 'blob:'],
childSrc: ["'self'", 'blob:'],
fontSrc: ["'self'", 'https://fonts.gstatic.com', 'data:'],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
frameAncestors: ["'self'"],
upgradeInsecureRequests: shouldForceHttps ? [] : null,
},
},
crossOriginEmbedderPolicy: false,
hsts: hstsActive ? { maxAge: 31536000, includeSubDomains: hstsIncludeSubdomains } : false,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}),
);
if (shouldForceHttps) {
app.use((req: Request, res: Response, next: NextFunction) => {
@@ -166,7 +178,19 @@ export function createApp(): express.Application {
// Request logging with sensitive field redaction
{
const SENSITIVE_KEYS = new Set(['password', 'new_password', 'current_password', 'token', 'jwt', 'authorization', 'cookie', 'client_secret', 'mfa_token', 'code', 'smtp_pass']);
const SENSITIVE_KEYS = new Set([
'password',
'new_password',
'current_password',
'token',
'jwt',
'authorization',
'cookie',
'client_secret',
'mfa_token',
'code',
'smtp_pass',
]);
const redact = (value: unknown): unknown => {
if (!value || typeof value !== 'object') return value;
if (Array.isArray(value)) return (value as unknown[]).map(redact);
@@ -245,12 +269,16 @@ export function createApp(): express.Application {
// Share-token path: require the token to cover the exact trip the
// photo belongs to. Expired tokens fall through to 401.
const photo = db.prepare('SELECT trip_id FROM photos WHERE filename = ?').get(safeName) as { trip_id: number } | undefined;
const photo = db.prepare('SELECT trip_id FROM photos WHERE filename = ?').get(safeName) as
| { trip_id: number }
| undefined;
if (!photo) return res.status(401).send('Authentication required');
const share = db.prepare(
"SELECT trip_id FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))"
).get(rawToken) as { trip_id: number } | undefined;
const share = db
.prepare(
"SELECT trip_id FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))",
)
.get(rawToken) as { trip_id: number } | undefined;
if (!share || share.trip_id !== photo.trip_id) {
return res.status(401).send('Authentication required');
}
@@ -277,8 +305,8 @@ export function createApp(): express.Application {
app.use('/api/trips/:tripId/reservations', reservationsRoutes);
app.use('/api/trips/:tripId/days/:dayId/notes', dayNotesRoutes);
app.get('/api/health', (_req: Request, res: Response) => {
res.setHeader('Cache-Control', 'no-store, must-revalidate')
res.json({ status: 'ok' })
res.setHeader('Cache-Control', 'no-store, must-revalidate');
res.json({ status: 'ok' });
});
app.use('/api/config', publicConfigRoutes);
app.use('/api', assignmentsRoutes);
@@ -288,18 +316,28 @@ export function createApp(): express.Application {
// Addons list endpoint
app.get('/api/addons', authenticate, (_req: Request, res: Response) => {
const addons = db.prepare('SELECT id, name, type, icon, enabled FROM addons WHERE enabled = 1 ORDER BY sort_order').all() as Pick<Addon, 'id' | 'name' | 'type' | 'icon' | 'enabled'>[];
const providers = db.prepare(`
const addons = db
.prepare('SELECT id, name, type, icon, enabled FROM addons WHERE enabled = 1 ORDER BY sort_order')
.all() as Pick<Addon, 'id' | 'name' | 'type' | 'icon' | 'enabled'>[];
const providers = db
.prepare(
`
SELECT id, name, icon, enabled, sort_order
FROM photo_providers
WHERE enabled = 1
ORDER BY sort_order, id
`).all() as Array<{ id: string; name: string; icon: string; enabled: number; sort_order: number }>;
const fields = db.prepare(`
`,
)
.all() as Array<{ id: string; name: string; icon: string; enabled: number; sort_order: number }>;
const fields = db
.prepare(
`
SELECT provider_id, field_key, label, input_type, placeholder, hint, required, secret, settings_key, payload_key, sort_order
FROM photo_provider_fields
ORDER BY sort_order, id
`).all() as Array<{
`,
)
.all() as Array<{
provider_id: string;
field_key: string;
label: string;
@@ -323,15 +361,15 @@ export function createApp(): express.Application {
res.json({
collabFeatures: getCollabFeatures(),
addons: [
...addons.map(a => ({ ...a, enabled: !!a.enabled })),
...providers.map(p => ({
...addons.map((a) => ({ ...a, enabled: !!a.enabled })),
...providers.map((p) => ({
id: p.id,
name: p.name,
type: 'photo_provider',
icon: p.icon,
enabled: !!p.enabled,
config: getPhotoProviderConfig(p.id),
fields: (fieldsByProvider.get(p.id) || []).map(f => ({
fields: (fieldsByProvider.get(p.id) || []).map((f) => ({
key: f.field_key,
label: f.label,
input_type: f.input_type,
@@ -351,10 +389,14 @@ export function createApp(): express.Application {
// Addon routes
app.use('/api/addons/vacay', vacayRoutes);
app.use('/api/addons/atlas', atlasRoutes);
app.use('/api/journeys', (req, res, next) => {
if (!isAddonEnabled(ADDON_IDS.JOURNEY)) return res.status(404).json({ error: 'Journey addon is not enabled' });
next();
}, journeyRoutes);
app.use(
'/api/journeys',
(req, res, next) => {
if (!isAddonEnabled(ADDON_IDS.JOURNEY)) return res.status(404).json({ error: 'Journey addon is not enabled' });
next();
},
journeyRoutes,
);
app.use('/api/public/journey', journeyPublicRoutes);
app.use('/api/integrations/memories', memoriesRoutes);
app.use('/api/photos', photoRoutes);
@@ -391,16 +433,16 @@ export function createApp(): express.Application {
if (_oauthMetadata) return _oauthMetadata;
const base = getMcpSafeUrl().replace(/\/+$/, '');
_oauthMetadata = {
issuer: base,
authorization_endpoint: `${base}/oauth/authorize`,
token_endpoint: `${base}/oauth/token`,
revocation_endpoint: `${base}/oauth/revoke`,
registration_endpoint: `${base}/oauth/register`,
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials'],
code_challenge_methods_supported: ['S256'],
issuer: base,
authorization_endpoint: `${base}/oauth/authorize`,
token_endpoint: `${base}/oauth/token`,
revocation_endpoint: `${base}/oauth/revoke`,
registration_endpoint: `${base}/oauth/register`,
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials'],
code_challenge_methods_supported: ['S256'],
token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
scopes_supported: ALL_SCOPES,
scopes_supported: ALL_SCOPES,
};
return _oauthMetadata;
}
@@ -446,11 +488,11 @@ export function createApp(): express.Application {
if (!isAddonEnabled(ADDON_IDS.MCP)) return res.status(404).end();
const meta = getOAuthMetadata();
res.json({
resource: `${meta.issuer}/mcp`,
authorization_servers: [meta.issuer],
resource: `${meta.issuer}/mcp`,
authorization_servers: [meta.issuer],
bearer_methods_supported: ['header'],
scopes_supported: ALL_SCOPES,
resource_name: 'TREK MCP',
scopes_supported: ALL_SCOPES,
resource_name: 'TREK MCP',
});
});
@@ -488,13 +530,15 @@ export function createApp(): express.Application {
// Production static file serving
if (process.env.NODE_ENV === 'production') {
const publicPath = path.join(__dirname, '../public');
app.use(express.static(publicPath, {
setHeaders: (res, filePath) => {
if (filePath.endsWith('index.html')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
}
},
}));
app.use(
express.static(publicPath, {
setHeaders: (res, filePath) => {
if (filePath.endsWith('index.html')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
}
},
}),
);
app.get('*', (_req: Request, res: Response) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.sendFile(path.join(publicPath, 'index.html'));
@@ -502,21 +546,23 @@ export function createApp(): express.Application {
}
// Global error handler
app.use((err: Error & { status?: number; statusCode?: number }, _req: Request, res: Response, _next: NextFunction) => {
if (process.env.NODE_ENV === 'production') {
console.error('Unhandled error:', err.message);
} else {
console.error('Unhandled error:', err);
}
if (err instanceof multer.MulterError) {
const status = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
return res.status(status).json({ error: err.message });
}
const status = err.statusCode || err.status || 500;
// Expose the message for client errors (4xx); keep 'Internal server error' for 5xx.
const message = status < 500 ? err.message : 'Internal server error';
res.status(status).json({ error: message });
});
app.use(
(err: Error & { status?: number; statusCode?: number }, _req: Request, res: Response, _next: NextFunction) => {
if (process.env.NODE_ENV === 'production') {
console.error('Unhandled error:', err.message);
} else {
console.error('Unhandled error:', err);
}
if (err instanceof multer.MulterError) {
const status = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
return res.status(status).json({ error: err.message });
}
const status = err.statusCode || err.status || 500;
// Expose the message for client errors (4xx); keep 'Internal server error' for 5xx.
const message = status < 500 ? err.message : 'Internal server error';
res.status(status).json({ error: message });
},
);
return app;
}
}