-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
163 lines (138 loc) · 5.58 KB
/
index.ts
File metadata and controls
163 lines (138 loc) · 5.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// IMPORTANT: Import Sentry instrumentation first, before any other modules
// This ensures pg and other modules are properly instrumented
import './instrument.js';
import type { ErrorResponse } from '@freundebuch/shared/index.js';
import { serve } from '@hono/node-server';
import * as Sentry from '@sentry/node';
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { secureHeaders } from 'hono/secure-headers';
import type pg from 'pg';
import { httpLoggerMiddleware } from './middleware/http-logger.js';
import { sentryTracingMiddleware } from './middleware/sentry.js';
import addressLookupRoutes from './routes/address-lookup.js';
import appPasswordsRoutes from './routes/app-passwords.js';
import authRoutes from './routes/auth.js';
import circlesRoutes from './routes/circles.js';
import collectivesRoutes from './routes/collectives.js';
import encountersRoutes from './routes/encounters.js';
import friendsRoutes from './routes/friends.js';
import healthRoutes from './routes/health.js';
import notificationChannelsRoutes from './routes/notification-channels.js';
import sentryTunnelRoutes from './routes/sentry-tunnel.js';
import uploadsRoutes from './routes/uploads.js';
import usersRoutes from './routes/users.js';
import { PhotoService } from './services/photo.service.js';
import type { AppContext } from './types/context.js';
import { initializeAddressCaches } from './utils/cache.js';
import { getConfig } from './utils/config.js';
import { checkDatabaseConnection, createPool } from './utils/db.js';
import { DatabaseConnectionError, isAppError, toError } from './utils/errors.js';
import { createLogger } from './utils/logger.js';
import { setupCleanupScheduler, setupNotificationScheduler } from './utils/scheduler.js';
Error.stackTraceLimit = 100;
export async function createApp(pool: pg.Pool) {
const config = getConfig();
const app = new Hono<AppContext>();
const logger = createLogger();
const dbConnected = await checkDatabaseConnection(pool);
if (dbConnected === false) {
throw new DatabaseConnectionError();
}
// Global context middleware - inject db and logger
app.use('*', async (c, next) => {
c.set('db', pool);
c.set('logger', logger.child({ requestId: crypto.randomUUID() }));
await next();
});
// Middleware
app.use('*', httpLoggerMiddleware);
app.use(
'*',
secureHeaders({
// Allow cross-origin resource loading (needed for frontend to load images from backend)
crossOriginResourcePolicy: 'cross-origin',
}),
);
app.use(
'*',
cors({
origin: config.FRONTEND_URL,
credentials: true,
// Allow Sentry tracing headers for distributed tracing
allowHeaders: ['Content-Type', 'Authorization', 'sentry-trace', 'baggage'],
exposeHeaders: ['sentry-trace', 'baggage'],
}),
);
// Sentry tracing middleware - must come after CORS to access headers
// This continues traces from the frontend for distributed tracing
app.use('*', sentryTracingMiddleware);
// Routes
app.route('/health', healthRoutes);
app.route('/api/auth', authRoutes);
app.route('/api/users', usersRoutes);
app.route('/api/friends', friendsRoutes);
app.route('/api/circles', circlesRoutes);
app.route('/api/collectives', collectivesRoutes);
app.route('/api/encounters', encountersRoutes);
app.route('/api/uploads', uploadsRoutes);
app.route('/api/app-passwords', appPasswordsRoutes);
app.route('/api/sentry-tunnel', sentryTunnelRoutes);
app.route('/api/address-lookup', addressLookupRoutes);
app.route('/api/notification-channels', notificationChannelsRoutes);
// Error handling
app.onError((err, c) => {
const pinoLogger = c.get('logger');
if (isAppError(err)) {
pinoLogger[err.statusCode >= 500 ? 'error' : 'warn']({ err }, err.message);
const body: ErrorResponse = { error: err.message };
if (err.code) body.code = err.code;
if (err.details !== undefined) body.details = err.details;
return c.json(body, err.statusCode);
}
const normalizedErr = toError(err);
pinoLogger.error({ err: normalizedErr }, 'Unhandled error');
Sentry.captureException(normalizedErr, {
extra: { path: c.req.path, method: c.req.method },
});
return c.json<ErrorResponse>({ error: 'Internal Server Error' }, 500);
});
// 404 handler
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404);
});
return app;
}
export async function startServer() {
const config = getConfig();
const pool = createPool();
const app = await createApp(pool);
const port = config.PORT;
const pinoLogger = createLogger();
// Migrate uploads directory from legacy 'contacts' path to 'friends'
await PhotoService.migrateFromLegacyPath(pinoLogger);
// Import setupGracefulShutdown lazily to avoid side effects
import('./utils/db.js').then(({ setupGracefulShutdown }) => {
setupGracefulShutdown(pool);
});
// Initialize address caches with database pool for persistence
initializeAddressCaches(pool, pinoLogger);
// Validate optional API keys at startup
if (!config.ZIPCODEBASE_API_KEY) {
pinoLogger.warn('ZIPCODEBASE_API_KEY not configured - address lookup will be disabled');
}
// Setup cleanup scheduler for expired sessions, tokens, and cache
setupCleanupScheduler(pool, pinoLogger);
// Setup notification scheduler for daily date digest messages
setupNotificationScheduler(pool, pinoLogger);
pinoLogger.info(`Starting server on port ${port}`);
serve({
fetch: app.fetch,
port,
});
return { app, port, logger: pinoLogger };
}
// Only start server if this file is being run directly
if (import.meta.main === true) {
startServer();
}