33 * Central routing setup for all API endpoints
44 */
55
6+ import rateLimit from 'express-rate-limit' ;
7+
68import authMiddleware from './middleware/authMiddleware.js' ;
79import antraegeRouter from './routes/antraege/index.js' ;
810import etherpadRoute from './routes/etherpad/etherpadController.js' ;
@@ -70,6 +72,27 @@ import { RouteStatsTracker } from './utils/routeStats.js';
7072
7173import type { Application , Request , Response , NextFunction } from 'express' ;
7274
75+ /**
76+ * IP-based rate limiters for abuse prevention.
77+ * These are intentionally softer since most routes also have frontend-side throttling.
78+ * Complements the existing Redis-based per-user rate limiter (used for business quotas).
79+ */
80+ const aiGenerationLimiter = rateLimit ( {
81+ windowMs : 15 * 60 * 1000 , // 15 minutes
82+ max : 60 , // ~4 per minute average
83+ standardHeaders : true ,
84+ legacyHeaders : false ,
85+ message : { error : 'Too many AI generation requests, please try again later.' } ,
86+ } ) ;
87+
88+ const standardMutationLimiter = rateLimit ( {
89+ windowMs : 15 * 60 * 1000 ,
90+ max : 200 ,
91+ standardHeaders : true ,
92+ legacyHeaders : false ,
93+ message : { error : 'Too many requests, please try again later.' } ,
94+ } ) ;
95+
7396const log = createLogger ( 'Routes' ) ;
7497
7598const { requireAuth } = authMiddleware ;
@@ -176,97 +199,135 @@ export async function setupRoutes(app: Application): Promise<void> {
176199 app . use ( '/api/scanner' , scannerRouter ) ;
177200 app . use ( '/api/protokoll' , protokollRouter ) ;
178201
179- app . use ( '/api/claude_social' , claudeSocialRoute ) ;
180- app . use ( '/api/claude_alttext' , claudeAlttextRoute ) ;
181- app . use ( '/api/claude_website' , claudeWebsiteRoute ) ;
182- app . use ( '/api/leichte_sprache' , leichteSpracheRoute ) ;
183- app . use ( '/api/claude_rede' , redeRouter ) ;
184- app . use ( '/api/claude_buergeranfragen' , buergeranfragenRouter ) ;
185- app . use ( '/api/claude_text_improver' , claudeTextImproverRoute ) ;
186- app . use ( '/api/chat' , grueneratorChatRoute ) ;
187- app . use ( '/api/chat-service' , chatServiceRouter ) ;
188- app . use ( '/api/chat-graph' , chatGraphRouter ) ;
189- app . use ( '/api/chat-deep' , chatDeepRouter ) ; // @experimental — DeepAgent route, not production-ready
202+ app . use ( '/api/claude_social' , aiGenerationLimiter , claudeSocialRoute ) ;
203+ app . use ( '/api/claude_alttext' , aiGenerationLimiter , claudeAlttextRoute ) ;
204+ app . use ( '/api/claude_website' , aiGenerationLimiter , claudeWebsiteRoute ) ;
205+ app . use ( '/api/leichte_sprache' , aiGenerationLimiter , leichteSpracheRoute ) ;
206+ app . use ( '/api/claude_rede' , aiGenerationLimiter , redeRouter ) ;
207+ app . use ( '/api/claude_buergeranfragen' , aiGenerationLimiter , buergeranfragenRouter ) ;
208+ app . use ( '/api/claude_text_improver' , aiGenerationLimiter , claudeTextImproverRoute ) ;
209+ app . use ( '/api/chat' , aiGenerationLimiter , grueneratorChatRoute ) ;
210+ app . use ( '/api/chat-service' , aiGenerationLimiter , chatServiceRouter ) ;
211+ app . use ( '/api/chat-graph' , aiGenerationLimiter , chatGraphRouter ) ;
212+ app . use ( '/api/chat-deep' , aiGenerationLimiter , chatDeepRouter ) ; // @experimental — DeepAgent route, not production-ready
190213 app . use ( '/api/gruen-o-mat' , gruenOMatRouter ) ;
191- app . use ( '/api/dreizeilen_canvas' , sharepicDreizeilenCanvasRoute ) ;
192- app . use ( '/api/zitat_canvas' , zitatSharepicCanvasRoute ) ;
193- app . use ( '/api/zitat_pure_canvas' , zitatPureSharepicCanvasRoute ) ;
194- app . use ( '/api/info_canvas' , infoSharepicCanvasRoute ) ;
195- app . use ( '/api/imagine_label_canvas' , imagineLabelCanvasRoute ) ;
196- app . use ( '/api/campaign_canvas' , campaignCanvasRoute ) ;
197- app . use ( '/api/veranstaltung_canvas' , veranstaltungCanvasRoute ) ;
198- app . use ( '/api/profilbild_canvas' , profilbildCanvasRoute ) ;
199- app . use ( '/api/simple_canvas' , simpleCanvasRoute ) ;
200- app . use ( '/api/slider_canvas' , sliderCanvasRoute ) ;
201- app . use ( '/api/campaign_generate' , campaignGenerateRoute ) ;
202- app . use ( '/api/dreizeilen_claude' , sharepicClaudeRoute ) ;
203- app . use ( '/api/sharepic/edit-session' , editSessionRouter ) ;
204- app . use ( '/api/sharepic' , promptRoute ) ;
214+ app . use ( '/api/dreizeilen_canvas' , standardMutationLimiter , sharepicDreizeilenCanvasRoute ) ;
215+ app . use ( '/api/zitat_canvas' , standardMutationLimiter , zitatSharepicCanvasRoute ) ;
216+ app . use ( '/api/zitat_pure_canvas' , standardMutationLimiter , zitatPureSharepicCanvasRoute ) ;
217+ app . use ( '/api/info_canvas' , standardMutationLimiter , infoSharepicCanvasRoute ) ;
218+ app . use ( '/api/imagine_label_canvas' , standardMutationLimiter , imagineLabelCanvasRoute ) ;
219+ app . use ( '/api/campaign_canvas' , standardMutationLimiter , campaignCanvasRoute ) ;
220+ app . use ( '/api/veranstaltung_canvas' , standardMutationLimiter , veranstaltungCanvasRoute ) ;
221+ app . use ( '/api/profilbild_canvas' , standardMutationLimiter , profilbildCanvasRoute ) ;
222+ app . use ( '/api/simple_canvas' , standardMutationLimiter , simpleCanvasRoute ) ;
223+ app . use ( '/api/slider_canvas' , standardMutationLimiter , sliderCanvasRoute ) ;
224+ app . use ( '/api/campaign_generate' , aiGenerationLimiter , campaignGenerateRoute ) ;
225+ app . use ( '/api/dreizeilen_claude' , aiGenerationLimiter , sharepicClaudeRoute ) ;
226+ app . use ( '/api/sharepic/edit-session' , standardMutationLimiter , editSessionRouter ) ;
227+ app . use ( '/api/sharepic' , aiGenerationLimiter , promptRoute ) ;
205228
206- app . post ( '/api/zitat_claude' , async ( req : Request , res : Response ) : Promise < void > => {
207- await handleClaudeRequest ( req as any , res , 'zitat' ) ;
208- } ) ;
209- app . post ( '/api/headline_claude' , async ( req : Request , res : Response ) : Promise < void > => {
210- await handleClaudeRequest ( req as any , res , 'headline' ) ;
211- } ) ;
212- app . post ( '/api/info_claude' , async ( req : Request , res : Response ) : Promise < void > => {
213- await handleClaudeRequest ( req as any , res , 'info' ) ;
214- } ) ;
215- app . post ( '/api/veranstaltung_claude' , async ( req : Request , res : Response ) : Promise < void > => {
216- await handleClaudeRequest ( req as any , res , 'veranstaltung' ) ;
217- } ) ;
218- app . post ( '/api/zitat_pure_claude' , async ( req : Request , res : Response ) : Promise < void > => {
219- await handleClaudeRequest ( req as any , res , 'zitat_pure' ) ;
220- } ) ;
221- app . post ( '/api/simple_claude' , async ( req : Request , res : Response ) : Promise < void > => {
222- await handleClaudeRequest ( req as any , res , 'simple' ) ;
223- } ) ;
224- app . post ( '/api/slider_claude' , async ( req : Request , res : Response ) : Promise < void > => {
225- if ( req . body . smartCount ) {
226- await handleSliderSmartRequest ( req as any , res ) ;
227- } else {
228- await handleClaudeRequest ( req as any , res , 'slider' ) ;
229+ app . post (
230+ '/api/zitat_claude' ,
231+ aiGenerationLimiter ,
232+ async ( req : Request , res : Response ) : Promise < void > => {
233+ await handleClaudeRequest ( req as any , res , 'zitat' ) ;
229234 }
230- } ) ;
231- app . post ( '/api/default_claude' , async ( req : Request , res : Response ) : Promise < void > => {
232- await handleClaudeRequest ( req as any , res , 'default' ) ;
233- } ) ;
235+ ) ;
236+ app . post (
237+ '/api/headline_claude' ,
238+ aiGenerationLimiter ,
239+ async ( req : Request , res : Response ) : Promise < void > => {
240+ await handleClaudeRequest ( req as any , res , 'headline' ) ;
241+ }
242+ ) ;
243+ app . post (
244+ '/api/info_claude' ,
245+ aiGenerationLimiter ,
246+ async ( req : Request , res : Response ) : Promise < void > => {
247+ await handleClaudeRequest ( req as any , res , 'info' ) ;
248+ }
249+ ) ;
250+ app . post (
251+ '/api/veranstaltung_claude' ,
252+ aiGenerationLimiter ,
253+ async ( req : Request , res : Response ) : Promise < void > => {
254+ await handleClaudeRequest ( req as any , res , 'veranstaltung' ) ;
255+ }
256+ ) ;
257+ app . post (
258+ '/api/zitat_pure_claude' ,
259+ aiGenerationLimiter ,
260+ async ( req : Request , res : Response ) : Promise < void > => {
261+ await handleClaudeRequest ( req as any , res , 'zitat_pure' ) ;
262+ }
263+ ) ;
264+ app . post (
265+ '/api/simple_claude' ,
266+ aiGenerationLimiter ,
267+ async ( req : Request , res : Response ) : Promise < void > => {
268+ await handleClaudeRequest ( req as any , res , 'simple' ) ;
269+ }
270+ ) ;
271+ app . post (
272+ '/api/slider_claude' ,
273+ aiGenerationLimiter ,
274+ async ( req : Request , res : Response ) : Promise < void > => {
275+ if ( req . body . smartCount ) {
276+ await handleSliderSmartRequest ( req as any , res ) ;
277+ } else {
278+ await handleClaudeRequest ( req as any , res , 'slider' ) ;
279+ }
280+ }
281+ ) ;
282+ app . post (
283+ '/api/default_claude' ,
284+ aiGenerationLimiter ,
285+ async ( req : Request , res : Response ) : Promise < void > => {
286+ await handleClaudeRequest ( req as any , res , 'default' ) ;
287+ }
288+ ) ;
234289
235- app . post ( '/api/generate-sharepic' , async ( req : Request , res : Response ) : Promise < void > => {
236- try {
237- const { type, ...requestBody } = req . body ;
238- if ( ! type ) {
239- res . status ( 400 ) . json ( { success : false , error : 'Sharepic type is required' } ) ;
240- return ;
290+ app . post (
291+ '/api/generate-sharepic' ,
292+ aiGenerationLimiter ,
293+ async ( req : Request , res : Response ) : Promise < void > => {
294+ try {
295+ const { type, ...requestBody } = req . body ;
296+ if ( ! type ) {
297+ res . status ( 400 ) . json ( { success : false , error : 'Sharepic type is required' } ) ;
298+ return ;
299+ }
300+ const result = await generateSharepicForChat ( req as any , type , requestBody ) ;
301+ res . json ( { success : true , ...result . content . sharepic , metadata : result . content . metadata } ) ;
302+ } catch ( error ) {
303+ const err = error instanceof Error ? error : new Error ( String ( error ) ) ;
304+ console . error ( '[UnifiedSharepic] Error:' , err ) ;
305+ res
306+ . status ( 500 )
307+ . json ( { success : false , error : err . message || 'Failed to generate sharepic' } ) ;
241308 }
242- const result = await generateSharepicForChat ( req as any , type , requestBody ) ;
243- res . json ( { success : true , ...result . content . sharepic , metadata : result . content . metadata } ) ;
244- } catch ( error ) {
245- const err = error instanceof Error ? error : new Error ( String ( error ) ) ;
246- console . error ( '[UnifiedSharepic] Error:' , err ) ;
247- res . status ( 500 ) . json ( { success : false , error : err . message || 'Failed to generate sharepic' } ) ;
248309 }
249- } ) ;
310+ ) ;
250311
251- app . use ( '/api/ai-image-modification' , aiImageModificationRouter ) ;
252- app . use ( '/api/imageupload' , imageUploadRouter ) ;
253- app . use ( '/api/processText' , processTextRouter ) ;
254- app . use ( '/api/claude_text_adjustment' , claudeTextAdjustmentRoute ) ;
255- app . use ( '/api/etherpad' , etherpadRoute ) ;
256- app . use ( '/api/claude_wahlprogramm' , wahlprogrammRouter ) ;
257- app . use ( '/api/claude_universal' , universalRouter ) ;
258- app . use ( '/api/texte/smart' , smartTexteRouter ) ;
259- app . use ( '/api/generate-content-title' , contentTitleRouter ) ;
260- app . use ( '/api/claude_gruene_jugend' , claudeGrueneJugendRoute ) ;
261- app . use ( '/api/claude_gruenerator_ask' , claudeGrueneratorAskRoute ) ;
312+ app . use ( '/api/ai-image-modification' , aiGenerationLimiter , aiImageModificationRouter ) ;
313+ app . use ( '/api/imageupload' , standardMutationLimiter , imageUploadRouter ) ;
314+ app . use ( '/api/processText' , aiGenerationLimiter , processTextRouter ) ;
315+ app . use ( '/api/claude_text_adjustment' , aiGenerationLimiter , claudeTextAdjustmentRoute ) ;
316+ app . use ( '/api/etherpad' , standardMutationLimiter , etherpadRoute ) ;
317+ app . use ( '/api/claude_wahlprogramm' , aiGenerationLimiter , wahlprogrammRouter ) ;
318+ app . use ( '/api/claude_universal' , aiGenerationLimiter , universalRouter ) ;
319+ app . use ( '/api/texte/smart' , aiGenerationLimiter , smartTexteRouter ) ;
320+ app . use ( '/api/generate-content-title' , aiGenerationLimiter , contentTitleRouter ) ;
321+ app . use ( '/api/claude_gruene_jugend' , aiGenerationLimiter , claudeGrueneJugendRoute ) ;
322+ app . use ( '/api/claude_gruenerator_ask' , aiGenerationLimiter , claudeGrueneratorAskRoute ) ;
262323 app . use ( '/api/custom_generator' , customGeneratorRoute ) ;
263324 app . use ( '/api/auth/custom_generator' , customGeneratorRoute ) ;
264325 app . use ( '/api/generate_generator_config' , generatorConfiguratorRoute ) ;
265326 app . use ( '/api/custom_prompt' , customPromptRoute ) ;
266327 app . use ( '/api/auth/custom_prompt' , customPromptRoute ) ;
267- app . use ( '/api/claude/generate-short-subtitles' , claudeSubtitlesRoute ) ;
268- app . use ( '/api/subtitler' , subtitlerRouter ) ;
269- app . use ( '/api/subtitler' , subtitlerSocialRouter ) ;
328+ app . use ( '/api/claude/generate-short-subtitles' , aiGenerationLimiter , claudeSubtitlesRoute ) ;
329+ app . use ( '/api/subtitler' , standardMutationLimiter , subtitlerRouter ) ;
330+ app . use ( '/api/subtitler' , standardMutationLimiter , subtitlerSocialRouter ) ;
270331 app . use ( '/api/subtitler/projects' , subtitlerProjectRouter ) ;
271332 app . use ( '/api/subtitler/share' , subtitlerShareRouter ) ;
272333 app . use ( '/api/share' , shareRouter ) ;
@@ -284,7 +345,7 @@ export async function setupRoutes(app: Application): Promise<void> {
284345 app . use ( '/api/unsplash' , unsplashRouter ) ;
285346 app . use ( '/api/web-search' , webSearchRouter ) ;
286347 app . use ( '/api/research' , requireAuth , researchRouter ) ;
287- app . use ( '/api/image-generation' , imageGenerationRouter ) ;
348+ app . use ( '/api/image-generation' , aiGenerationLimiter , imageGenerationRouter ) ;
288349 app . use ( '/api/rate-limit' , rateLimitRouter ) ;
289350
290351 // Debug: log all requests to /api/releases/*
@@ -320,11 +381,11 @@ export async function setupRoutes(app: Application): Promise<void> {
320381
321382 app . use ( '/api/video' , requireAuth , videoRouter ) ;
322383 app . use ( '/api/nextcloud' , nextcloudApiRouter ) ;
323- app . use ( '/api/sites/generate-from-flyer' , flyerController ) ;
324- app . use ( '/api/sites' , sitesRouter ) ;
325- app . use ( '/api/flux/green-edit' , fluxImageEditingRoute ) ;
326- app . use ( '/api/imagine/create' , imagineCreateRoute ) ;
327- app . use ( '/api/imagine/pure' , imaginePureRoute ) ;
384+ app . use ( '/api/sites/generate-from-flyer' , aiGenerationLimiter , flyerController ) ;
385+ app . use ( '/api/sites' , standardMutationLimiter , sitesRouter ) ;
386+ app . use ( '/api/flux/green-edit' , aiGenerationLimiter , fluxImageEditingRoute ) ;
387+ app . use ( '/api/imagine/create' , aiGenerationLimiter , imagineCreateRoute ) ;
388+ app . use ( '/api/imagine/pure' , aiGenerationLimiter , imaginePureRoute ) ;
328389
329390 // Web redirect to frontend imagine (KI image studio)
330391 app . get ( '/web' , ( req : Request , res : Response ) => {
0 commit comments