@@ -7,6 +7,7 @@ import fs from 'fs';
77import path from 'path' ;
88
99import express , { type Request , type Response , type Router } from 'express' ;
10+ import rateLimit from 'express-rate-limit' ;
1011import sharp from 'sharp' ;
1112
1213import { requireAuth } from '../../middleware/authMiddleware.js' ;
@@ -19,6 +20,14 @@ import type { SharedMediaRow, ShareResult } from '../../types/media.js';
1920const fsPromises = fs . promises ;
2021const log = createLogger ( 'share' ) ;
2122
23+ // Rate limiters for share mutation routes
24+ const shareWriteLimiter = rateLimit ( {
25+ windowMs : 15 * 60 * 1000 , // 15 minutes
26+ max : 50 ,
27+ standardHeaders : true ,
28+ legacyHeaders : false ,
29+ } ) ;
30+
2231// ============================================================================
2332// Types
2433// ============================================================================
@@ -57,7 +66,11 @@ interface SharedMediaService {
5766 userId : string ,
5867 params : CreatePendingVideoShareParams
5968 ) : Promise < ShareResult > ;
60- getUserShares ( userId : string , type : string | null ) : Promise < SharedMediaRow [ ] > ;
69+ getUserShares (
70+ userId : string ,
71+ type : string | null ,
72+ status ?: string | null
73+ ) : Promise < SharedMediaRow [ ] > ;
6174 getUserShareCount ( userId : string ) : Promise < number > ;
6275 getShareByToken ( shareToken : string ) : Promise < SharedMediaRow | null > ;
6376 recordView ( shareToken : string ) : Promise < void > ;
@@ -103,6 +116,7 @@ interface CreateImageShareParams {
103116 imageType : string | null ;
104117 metadata : Record < string , unknown > ;
105118 originalImage : string | null ;
119+ status ?: 'ready' | 'draft' ;
106120}
107121
108122interface CreateVideoShareParams {
@@ -134,6 +148,7 @@ interface ImageShareRequest extends AuthenticatedRequest {
134148 imageType ?: string ;
135149 metadata ?: Record < string , unknown > ;
136150 originalImage ?: string ;
151+ status ?: 'ready' | 'draft' ;
137152 } ;
138153}
139154
@@ -299,53 +314,111 @@ async function triggerBackgroundRender(
299314
300315const router : Router = express . Router ( ) ;
301316
317+ // Override global body parser limit for share routes (canvas images can exceed 10MB)
318+ router . use ( express . json ( { limit : '50mb' } ) ) ;
319+
302320// ============================================================================
303321// IMAGE SHARE ROUTES
304322// ============================================================================
305323
306- router . post ( '/image' , requireAuth , async ( req : ImageShareRequest , res : Response < ShareResponse > ) => {
307- try {
308- const userId = req . user ! . id ;
309- const { imageData, title, imageType, metadata, originalImage } = req . body ;
324+ router . post (
325+ '/image' ,
326+ shareWriteLimiter ,
327+ requireAuth ,
328+ async ( req : ImageShareRequest , res : Response < ShareResponse > ) => {
329+ try {
330+ const userId = req . user ! . id ;
331+ const { imageData, title, imageType, metadata, originalImage, status } = req . body ;
310332
311- if ( ! imageData ) {
312- return res . status ( 400 ) . json ( {
333+ if ( ! imageData ) {
334+ return res . status ( 400 ) . json ( {
335+ success : false ,
336+ error : 'Bilddaten werden benötigt' ,
337+ } ) ;
338+ }
339+
340+ const service = await getSharedMediaService ( ) ;
341+ const share = await service . createImageShare ( userId , {
342+ imageBase64 : imageData ,
343+ title : title || 'Geteiltes Bild' ,
344+ imageType : imageType || null ,
345+ metadata : metadata || { } ,
346+ originalImage : originalImage || null ,
347+ status : status === 'draft' ? 'draft' : 'ready' ,
348+ } ) ;
349+
350+ log . info (
351+ `Image share created: ${ share . shareToken } by user ${ userId } ${ originalImage ? ' (with original)' : '' } `
352+ ) ;
353+
354+ return res . json ( {
355+ success : true ,
356+ share : {
357+ shareToken : share . shareToken ,
358+ shareUrl : share . shareUrl ,
359+ createdAt : share . createdAt ,
360+ mediaType : 'image' ,
361+ hasOriginalImage : share . hasOriginalImage || false ,
362+ } ,
363+ } ) ;
364+ } catch ( error ) {
365+ log . error ( 'Failed to create image share:' , error ) ;
366+ return res . status ( 500 ) . json ( {
313367 success : false ,
314- error : 'Bilddaten werden benötigt ' ,
368+ error : 'Bild konnte nicht geteilt werden ' ,
315369 } ) ;
316370 }
371+ }
372+ ) ;
317373
318- const service = await getSharedMediaService ( ) ;
319- const share = await service . createImageShare ( userId , {
320- imageBase64 : imageData ,
321- title : title || 'Geteiltes Bild' ,
322- imageType : imageType || null ,
323- metadata : metadata || { } ,
324- originalImage : originalImage || null ,
325- } ) ;
374+ // Promote a draft to ready (publish)
375+ router . put (
376+ '/:shareToken/publish' ,
377+ shareWriteLimiter ,
378+ requireAuth ,
379+ async ( req : Request < ShareTokenParams > , res : Response < ShareResponse > ) => {
380+ try {
381+ const userId = ( req as AuthenticatedRequest ) . user ! . id ;
382+ const { shareToken } = req . params ;
326383
327- log . info (
328- `Image share created: ${ share . shareToken } by user ${ userId } ${ originalImage ? ' (with original)' : '' } `
329- ) ;
384+ const service = await getSharedMediaService ( ) ;
385+ const share = await service . getShareByToken ( shareToken as string ) ;
330386
331- return res . json ( {
332- success : true ,
333- share : {
334- shareToken : share . shareToken ,
335- shareUrl : share . shareUrl ,
336- createdAt : share . createdAt ,
337- mediaType : 'image' ,
338- hasOriginalImage : share . hasOriginalImage || false ,
339- } ,
340- } ) ;
341- } catch ( error ) {
342- log . error ( 'Failed to create image share:' , error ) ;
343- return res . status ( 500 ) . json ( {
344- success : false ,
345- error : 'Bild konnte nicht geteilt werden' ,
346- } ) ;
387+ if ( ! share ) {
388+ return res . status ( 404 ) . json ( { success : false , error : 'Share nicht gefunden' } ) ;
389+ }
390+
391+ if ( share . user_id !== userId ) {
392+ return res . status ( 403 ) . json ( { success : false , error : 'Nicht berechtigt' } ) ;
393+ }
394+
395+ const pg = ( await import ( '../../database/services/PostgresService.js' ) ) . getPostgresInstance ( ) ;
396+ await pg . query ( 'UPDATE shared_media SET status = $1 WHERE share_token = $2' , [
397+ 'ready' ,
398+ shareToken ,
399+ ] ) ;
400+
401+ log . info ( `Share ${ shareToken } published by user ${ userId } ` ) ;
402+
403+ return res . json ( {
404+ success : true ,
405+ share : {
406+ shareToken : share . share_token ,
407+ shareUrl : `/share/${ shareToken } ` ,
408+ createdAt : share . created_at ,
409+ mediaType : share . media_type as 'image' | 'video' ,
410+ status : 'ready' ,
411+ } ,
412+ } ) ;
413+ } catch ( error ) {
414+ log . error ( 'Failed to publish share:' , error ) ;
415+ return res . status ( 500 ) . json ( {
416+ success : false ,
417+ error : 'Share konnte nicht veröffentlicht werden' ,
418+ } ) ;
419+ }
347420 }
348- } ) ;
421+ ) ;
349422
350423// ============================================================================
351424// VIDEO SHARE ROUTES
@@ -580,9 +653,10 @@ router.get(
580653 try {
581654 const userId = req . user ! . id ;
582655 const type = req . query . type as string | undefined ;
656+ const status = req . query . status as string | undefined ;
583657
584658 const service = await getSharedMediaService ( ) ;
585- const shares = await service . getUserShares ( userId , type || null ) ;
659+ const shares = await service . getUserShares ( userId , type || null , status || null ) ;
586660 const count = await service . getUserShareCount ( userId ) ;
587661
588662 res . json ( {
0 commit comments