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
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};