This repository has no description
9.7 kB
259 lines
1import express, { Express } from 'express';
2import cors from 'cors';
3import cookieParser from 'cookie-parser';
4import { Router } from 'express';
5import * as Sentry from '@sentry/node';
6import { createUserRoutes } from '../../../modules/user/infrastructure/http/routes/userRoutes';
7import { createStatsRoutes } from '../../../modules/user/infrastructure/http/routes/statsRoutes';
8import { createAtprotoRoutes } from '../../../modules/atproto/infrastructure/atprotoRoutes';
9import { createCardsModuleRoutes } from '../../../modules/cards/infrastructure/http/routes';
10import { createConnectionRoutes } from '../../../modules/cards/infrastructure/http/routes/connectionRoutes';
11import { createGraphRoutes } from '../../../modules/cards/infrastructure/http/routes/graphRoutes';
12import { createFeedRoutes } from '../../../modules/feeds/infrastructure/http/routes/feedRoutes';
13import { createSearchRoutes } from '../../../modules/search/infrastructure/http/routes/searchRoutes';
14import { createNotificationRoutes } from '../../../modules/notifications/infrastructure/http/routes/notificationRoutes';
15import { createTestRoutes } from './routes/testRoutes';
16import {
17 EnvironmentConfigService,
18 Environment,
19} from '../config/EnvironmentConfigService';
20import { RepositoryFactory } from './factories/RepositoryFactory';
21import { ServiceFactory } from './factories/ServiceFactory';
22import { UseCaseFactory } from './factories/UseCaseFactory';
23import { ControllerFactory } from './factories/ControllerFactory';
24
25export const createExpressApp = (
26 configService: EnvironmentConfigService,
27): Express => {
28 const app = express();
29
30 // Determine allowed origins based on environment
31 const getAllowedOrigins = () => {
32 const environment = configService.get().environment;
33 const appUrl = configService.getAppConfig().appUrl;
34
35 switch (environment) {
36 case Environment.PROD:
37 return ['https://semble.so', 'https://api.semble.so'];
38 case Environment.DEV:
39 return ['https://dev.semble.so', 'https://api.dev.semble.so'];
40 case Environment.LOCAL:
41 default:
42 // Allow both localhost:4000 and configured appUrl for flexibility
43 return [
44 'http://localhost:4000',
45 'http://127.0.0.1:4000',
46 appUrl,
47 'http://localhost:3000',
48 'http://127.0.0.1:3000',
49 ];
50 }
51 };
52
53 const environment = configService.get().environment;
54 const allowedOrigins = getAllowedOrigins();
55 const allowedMethods = ['GET', 'POST', 'PUT', 'DELETE'];
56 const allowedOriginsSet = new Set(allowedOrigins);
57
58 // PROD: strict allowlist only — no open-CORS fallback.
59 // DEV/LOCAL: credentialed CORS for known origins, open wildcard for everyone else
60 // so public read endpoints are callable from any third-party origin.
61 // We use an origin callback rather than a custom middleware to avoid Fly.io's
62 // proxy rewriting the Origin header and then stripping the reflected ACAO header.
63 app.use(
64 cors({
65 origin: (origin, callback) => {
66 if (!origin || allowedOriginsSet.has(origin)) {
67 // No origin (server-to-server) or known origin: allow with credentials
68 callback(null, origin || true);
69 } else if (environment === Environment.PROD) {
70 // Prod: reject unknown origins entirely
71 callback(new Error(`Origin ${origin} not allowed`));
72 } else {
73 // DEV/LOCAL: allow unknown origins without credentials (open read access)
74 callback(null, '*');
75 }
76 },
77 credentials: true,
78 methods: allowedMethods,
79 }),
80 );
81
82 // Middleware setup
83 app.use(cookieParser()); // Parse cookies from incoming requests
84 app.use(express.json());
85 app.use(express.urlencoded({ extended: true }));
86
87 // Create all dependencies using factories
88 const repositories = RepositoryFactory.create(configService);
89 const services = ServiceFactory.createForWebApp(configService, repositories);
90 const useCases = UseCaseFactory.createForWebApp(repositories, services);
91
92 // Construct serviceDid for XRPC
93 const baseUrl = configService.getAtProtoConfig().baseUrl;
94 const serviceDid = `did:web:${baseUrl.replace(/^https?:\/\//, '')}`;
95
96 const controllers = ControllerFactory.create(
97 useCases,
98 services.cookieService,
99 services,
100 repositories,
101 configService.getAppConfig().appUrl,
102 serviceDid,
103 );
104
105 // Routes
106 const userRouter = Router();
107 const atprotoRouter = Router();
108
109 createUserRoutes(
110 userRouter,
111 services.authMiddleware,
112 controllers.initiateOAuthSignInController,
113 controllers.completeOAuthSignInController,
114 controllers.loginWithAppPasswordController,
115 controllers.logoutController,
116 controllers.getMyProfileController,
117 controllers.getUserProfileController,
118 controllers.refreshAccessTokenController,
119 controllers.generateExtensionTokensController,
120 controllers.followTargetController,
121 controllers.unfollowTargetController,
122 controllers.getFollowingUsersController,
123 controllers.getFollowersController,
124 controllers.getFollowingCollectionsController,
125 controllers.getFollowingCountController,
126 controllers.getFollowersCountController,
127 controllers.getFollowingCollectionsCountController,
128 );
129
130 createAtprotoRoutes(atprotoRouter, services.nodeOauthClient);
131
132 const cardsRouter = createCardsModuleRoutes(
133 services.authMiddleware,
134 // Card controllers
135 controllers.addUrlToLibraryController,
136 controllers.addCardToLibraryController,
137 controllers.addCardToCollectionController,
138 controllers.updateNoteCardController,
139 controllers.updateUrlCardAssociationsController,
140 controllers.removeCardFromLibraryController,
141 controllers.removeCardFromCollectionController,
142 controllers.getUrlMetadataController,
143 controllers.getUrlCardViewController,
144 controllers.getLibrariesForCardController,
145 controllers.getMyUrlCardsController,
146 controllers.getUserUrlCardsController,
147 controllers.getUrlStatusForMyLibraryController,
148 controllers.getLibrariesForUrlController,
149 controllers.getNoteCardsForUrlController,
150 controllers.searchUrlsController,
151 // Collection controllers
152 controllers.createCollectionController,
153 controllers.updateCollectionController,
154 controllers.deleteCollectionController,
155 controllers.getCollectionPageController,
156 controllers.getCollectionPageByAtUriController,
157 controllers.getMyCollectionsController,
158 controllers.getCollectionsController,
159 controllers.getCollectionsForUrlController,
160 controllers.searchCollectionsController,
161 controllers.getOpenCollectionsWithContributorController,
162 controllers.getCollectionFollowersController,
163 controllers.getCollectionFollowersCountController,
164 controllers.getCollectionContributorsController,
165 );
166
167 const connectionRouter = createConnectionRoutes(
168 services.authMiddleware,
169 controllers.createConnectionController,
170 controllers.updateConnectionController,
171 controllers.deleteConnectionController,
172 controllers.getConnectionsController,
173 controllers.getConnectionsForUrlController,
174 );
175
176 const graphRouter = createGraphRoutes(
177 services.authMiddleware,
178 controllers.getGraphDataController,
179 controllers.getUserGraphDataController,
180 controllers.getUrlGraphDataController,
181 );
182
183 const feedRouter = createFeedRoutes(
184 services.authMiddleware,
185 controllers.getGlobalFeedController,
186 controllers.getGemActivityFeedController,
187 controllers.getFollowingFeedController,
188 );
189
190 const searchRouter = createSearchRoutes(
191 services.authMiddleware,
192 controllers.getSimilarUrlsForUrlController,
193 controllers.searchBskyPostsForUrlController,
194 controllers.semanticSearchUrlsController,
195 controllers.searchAtProtoAccountsController,
196 controllers.searchLeafletDocsForUrlController,
197 );
198
199 const notificationRouter = createNotificationRoutes(
200 services.authMiddleware,
201 controllers.getMyNotificationsController,
202 controllers.getUnreadNotificationCountController,
203 controllers.markNotificationsAsReadController,
204 controllers.markAllNotificationsAsReadController,
205 );
206
207 const testRouter = Router();
208 createTestRoutes(testRouter);
209
210 const statsRouter = Router();
211 createStatsRoutes(
212 statsRouter,
213 services.statsApiKeyMiddleware,
214 controllers.getUserStatsController,
215 );
216
217 // OAuth client metadata endpoint
218 app.get('/oauth-client-metadata.json', (req, res) => {
219 res.json(services.nodeOauthClient.clientMetadata);
220 });
221
222 // DID Web endpoint
223 app.get('/.well-known/did.json', (req, res) => {
224 return res.json({
225 '@context': ['https://www.w3.org/ns/did/v1'],
226 id: serviceDid,
227 service: [
228 {
229 id: '#mention_search',
230 type: 'MentionSearchService',
231 serviceEndpoint: `https://${baseUrl.replace(/^https?:\/\//, '')}`,
232 },
233 ],
234 });
235 });
236
237 // XRPC mention search endpoint
238 app.get('/xrpc/parts.page.mention.search', (req, res) => {
239 console.log('Received XRPC mention search request with query:', req.query);
240 return controllers.pagePartsSearchController.execute(req, res);
241 });
242
243 // Register routes
244 app.use('/api/users', userRouter);
245 app.use('/atproto', atprotoRouter);
246 app.use('/api', cardsRouter);
247 app.use('/api/connections', connectionRouter);
248 app.use('/api/graph', graphRouter);
249 app.use('/api/feeds', feedRouter);
250 app.use('/api/search', searchRouter);
251 app.use('/api/notifications', notificationRouter);
252 app.use('/api/test', testRouter);
253 app.use('/api/stats', statsRouter);
254
255 // Sentry error handler - must be after all routes and before other error middleware
256 Sentry.setupExpressErrorHandler(app);
257
258 return app;
259};