Skip to content

Commit ab09844

Browse files
authored
Merge pull request #532 from netzbegruenung/test-branch
Test branch
2 parents f2ca566 + 6c5fec9 commit ab09844

9 files changed

Lines changed: 160 additions & 90 deletions

File tree

.github/workflows/build-images.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ jobs:
101101
sites:
102102
- 'apps/sites/**'
103103
- 'packages/shared/**'
104+
- 'packages/sites-design/**'
104105
docs:
105106
- 'apps/docs/**'
106107
- 'packages/shared/**'

apps/api/routes.ts

Lines changed: 146 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* Central routing setup for all API endpoints
44
*/
55

6+
import rateLimit from 'express-rate-limit';
7+
68
import authMiddleware from './middleware/authMiddleware.js';
79
import antraegeRouter from './routes/antraege/index.js';
810
import etherpadRoute from './routes/etherpad/etherpadController.js';
@@ -70,6 +72,27 @@ import { RouteStatsTracker } from './utils/routeStats.js';
7072

7173
import 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+
7396
const log = createLogger('Routes');
7497

7598
const { 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) => {

apps/docs/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
2727
COPY packages/shared ./packages/shared
2828
COPY packages/docs ./packages/docs
2929
COPY apps/web/src/assets/styles ./apps/web/src/assets/styles
30+
COPY apps/web/src/assets/fonts ./apps/web/src/assets/fonts
3031
COPY apps/docs ./apps/docs
3132

3233
# Build the Vite app

apps/sites/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ COPY tsconfig.base.json ./
1717

1818
# Copy only package.json files needed by pnpm filter (better layer caching)
1919
COPY packages/shared/package.json ./packages/shared/
20+
COPY packages/sites-design/package.json ./packages/sites-design/
2021
COPY apps/sites/package.json ./apps/sites/
2122

2223
# Install only sites + transitive workspace deps
@@ -25,6 +26,7 @@ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
2526

2627
# Copy source (changes here don't bust the install cache)
2728
COPY packages/shared ./packages/shared
29+
COPY packages/sites-design ./packages/sites-design
2830
COPY apps/sites ./apps/sites
2931

3032
# Build environment variables (can be overridden at build time)

apps/web/src/components/ui/popover.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function PopoverContent({
1515
className,
1616
align = 'center',
1717
sideOffset = 4,
18+
collisionPadding = 8,
1819
...props
1920
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
2021
return (
@@ -23,6 +24,7 @@ function PopoverContent({
2324
data-slot="popover-content"
2425
align={align}
2526
sideOffset={sideOffset}
27+
collisionPadding={collisionPadding}
2628
className={cn(
2729
'bg-background-pure z-50 w-72 origin-(--radix-popover-content-transform-origin) overflow-hidden rounded-md border border-grey-200 p-1 shadow-lg outline-hidden dark:border-grey-700 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
2830
className

apps/web/src/features/research/ResearchPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ function ResearchPage() {
142142
<ErrorBoundary>
143143
<div className="gallery-layout">
144144
<div className="gallery-header">
145-
<h1>Research</h1>
145+
<h1>Recherche</h1>
146146
<p>
147147
Durchsuche alle gescrapten Dokumente und Programme direkt in den Qdrant-Kollektionen.
148148
</p>
@@ -241,4 +241,4 @@ function ResearchPage() {
241241
);
242242
}
243243

244-
export default withAuthRequired(ResearchPage, { title: 'Research' });
244+
export default withAuthRequired(ResearchPage, { title: 'Recherche' });

apps/web/src/features/research/components/ResearchFilterPanel.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ export default function ResearchFilterPanel({
139139
)}
140140
</Button>
141141
</PopoverTrigger>
142-
<PopoverContent align="start" className="w-[22rem] max-h-[28rem] overflow-y-auto p-3">
142+
<PopoverContent
143+
align="start"
144+
className="w-[22rem] sm:w-[30rem] max-h-[28rem] sm:max-h-[32rem] overflow-y-auto p-3"
145+
>
143146
<div className="space-y-3">
144147
<div className="flex items-center justify-between">
145148
<span className="text-sm font-medium">

apps/web/src/features/research/components/ResearchSettingsPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export default function ResearchSettingsPanel({
137137
<PopoverContent
138138
align="end"
139139
sideOffset={8}
140-
className="w-[22rem] max-h-[32rem] overflow-y-auto p-3"
140+
className="w-[22rem] sm:w-[30rem] max-h-[32rem] sm:max-h-[36rem] overflow-y-auto p-3"
141141
>
142142
<div className="space-y-3">
143143
<div className="flex items-center justify-between">

apps/web/src/features/tools/ToolsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const tools: ToolEntry[] = [
3030
},
3131
{
3232
id: 'research',
33-
title: 'Research',
33+
title: 'Recherche',
3434
description: 'Manuelle Suche über alle gescrapten Dokumente und Programme.',
3535
path: '/research',
3636
tags: ['Dokumente', 'Qdrant'],

0 commit comments

Comments
 (0)