This repository has no description
10 kB
273 lines
1import express, { Express, Request, Response, NextFunction } 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
57 if (environment === Environment.PROD) {
58 app.use(
59 cors({
60 origin: allowedOrigins,
61 methods: allowedMethods,
62 credentials: true, // Required for cookies to work in cross-origin requests
63 }),
64 );
65 } else {
66 // DEV/LOCAL: allow credentialed CORS for known origins, open CORS for everyone else.
67 // Non-allowed origins get '*' which browsers refuse to use with credentials —
68 // so cookie-auth endpoints naturally fail for them, while read-only endpoints work.
69 const allowedOriginsSet = new Set(allowedOrigins);
70 app.use((req: Request, res: Response, next: NextFunction) => {
71 const origin = req.headers.origin;
72
73 if (typeof origin === 'string' && allowedOriginsSet.has(origin)) {
74 res.setHeader('Access-Control-Allow-Origin', origin);
75 res.setHeader('Access-Control-Allow-Credentials', 'true');
76 res.setHeader('Vary', 'Origin');
77 } else {
78 res.setHeader('Access-Control-Allow-Origin', '*');
79 }
80
81 res.setHeader('Access-Control-Allow-Methods', allowedMethods.join(','));
82 res.setHeader(
83 'Access-Control-Allow-Headers',
84 'Content-Type,Authorization',
85 );
86
87 if (req.method === 'OPTIONS') {
88 res.status(204).end();
89 return;
90 }
91
92 next();
93 });
94 }
95
96 // Middleware setup
97 app.use(cookieParser()); // Parse cookies from incoming requests
98 app.use(express.json());
99 app.use(express.urlencoded({ extended: true }));
100
101 // Create all dependencies using factories
102 const repositories = RepositoryFactory.create(configService);
103 const services = ServiceFactory.createForWebApp(configService, repositories);
104 const useCases = UseCaseFactory.createForWebApp(repositories, services);
105
106 // Construct serviceDid for XRPC
107 const baseUrl = configService.getAtProtoConfig().baseUrl;
108 const serviceDid = `did:web:${baseUrl.replace(/^https?:\/\//, '')}`;
109
110 const controllers = ControllerFactory.create(
111 useCases,
112 services.cookieService,
113 services,
114 repositories,
115 configService.getAppConfig().appUrl,
116 serviceDid,
117 );
118
119 // Routes
120 const userRouter = Router();
121 const atprotoRouter = Router();
122
123 createUserRoutes(
124 userRouter,
125 services.authMiddleware,
126 controllers.initiateOAuthSignInController,
127 controllers.completeOAuthSignInController,
128 controllers.loginWithAppPasswordController,
129 controllers.logoutController,
130 controllers.getMyProfileController,
131 controllers.getUserProfileController,
132 controllers.refreshAccessTokenController,
133 controllers.generateExtensionTokensController,
134 controllers.followTargetController,
135 controllers.unfollowTargetController,
136 controllers.getFollowingUsersController,
137 controllers.getFollowersController,
138 controllers.getFollowingCollectionsController,
139 controllers.getFollowingCountController,
140 controllers.getFollowersCountController,
141 controllers.getFollowingCollectionsCountController,
142 );
143
144 createAtprotoRoutes(atprotoRouter, services.nodeOauthClient);
145
146 const cardsRouter = createCardsModuleRoutes(
147 services.authMiddleware,
148 // Card controllers
149 controllers.addUrlToLibraryController,
150 controllers.addCardToLibraryController,
151 controllers.addCardToCollectionController,
152 controllers.updateNoteCardController,
153 controllers.updateUrlCardAssociationsController,
154 controllers.removeCardFromLibraryController,
155 controllers.removeCardFromCollectionController,
156 controllers.getUrlMetadataController,
157 controllers.getUrlCardViewController,
158 controllers.getLibrariesForCardController,
159 controllers.getMyUrlCardsController,
160 controllers.getUserUrlCardsController,
161 controllers.getUrlStatusForMyLibraryController,
162 controllers.getLibrariesForUrlController,
163 controllers.getNoteCardsForUrlController,
164 controllers.searchUrlsController,
165 // Collection controllers
166 controllers.createCollectionController,
167 controllers.updateCollectionController,
168 controllers.deleteCollectionController,
169 controllers.getCollectionPageController,
170 controllers.getCollectionPageByAtUriController,
171 controllers.getMyCollectionsController,
172 controllers.getCollectionsController,
173 controllers.getCollectionsForUrlController,
174 controllers.searchCollectionsController,
175 controllers.getOpenCollectionsWithContributorController,
176 controllers.getCollectionFollowersController,
177 controllers.getCollectionFollowersCountController,
178 controllers.getCollectionContributorsController,
179 );
180
181 const connectionRouter = createConnectionRoutes(
182 services.authMiddleware,
183 controllers.createConnectionController,
184 controllers.updateConnectionController,
185 controllers.deleteConnectionController,
186 controllers.getConnectionsController,
187 controllers.getConnectionsForUrlController,
188 );
189
190 const graphRouter = createGraphRoutes(
191 services.authMiddleware,
192 controllers.getGraphDataController,
193 controllers.getUserGraphDataController,
194 controllers.getUrlGraphDataController,
195 );
196
197 const feedRouter = createFeedRoutes(
198 services.authMiddleware,
199 controllers.getGlobalFeedController,
200 controllers.getGemActivityFeedController,
201 controllers.getFollowingFeedController,
202 );
203
204 const searchRouter = createSearchRoutes(
205 services.authMiddleware,
206 controllers.getSimilarUrlsForUrlController,
207 controllers.searchBskyPostsForUrlController,
208 controllers.semanticSearchUrlsController,
209 controllers.searchAtProtoAccountsController,
210 controllers.searchLeafletDocsForUrlController,
211 );
212
213 const notificationRouter = createNotificationRoutes(
214 services.authMiddleware,
215 controllers.getMyNotificationsController,
216 controllers.getUnreadNotificationCountController,
217 controllers.markNotificationsAsReadController,
218 controllers.markAllNotificationsAsReadController,
219 );
220
221 const testRouter = Router();
222 createTestRoutes(testRouter);
223
224 const statsRouter = Router();
225 createStatsRoutes(
226 statsRouter,
227 services.statsApiKeyMiddleware,
228 controllers.getUserStatsController,
229 );
230
231 // OAuth client metadata endpoint
232 app.get('/oauth-client-metadata.json', (req, res) => {
233 res.json(services.nodeOauthClient.clientMetadata);
234 });
235
236 // DID Web endpoint
237 app.get('/.well-known/did.json', (req, res) => {
238 return res.json({
239 '@context': ['https://www.w3.org/ns/did/v1'],
240 id: serviceDid,
241 service: [
242 {
243 id: '#mention_search',
244 type: 'MentionSearchService',
245 serviceEndpoint: `https://${baseUrl.replace(/^https?:\/\//, '')}`,
246 },
247 ],
248 });
249 });
250
251 // XRPC mention search endpoint
252 app.get('/xrpc/parts.page.mention.search', (req, res) => {
253 console.log('Received XRPC mention search request with query:', req.query);
254 return controllers.pagePartsSearchController.execute(req, res);
255 });
256
257 // Register routes
258 app.use('/api/users', userRouter);
259 app.use('/atproto', atprotoRouter);
260 app.use('/api', cardsRouter);
261 app.use('/api/connections', connectionRouter);
262 app.use('/api/graph', graphRouter);
263 app.use('/api/feeds', feedRouter);
264 app.use('/api/search', searchRouter);
265 app.use('/api/notifications', notificationRouter);
266 app.use('/api/test', testRouter);
267 app.use('/api/stats', statsRouter);
268
269 // Sentry error handler - must be after all routes and before other error middleware
270 Sentry.setupExpressErrorHandler(app);
271
272 return app;
273};