This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / shared / infrastructure / http / app.ts
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};