Initial commit - Project start (July 17, 2025)
Mirror to GitHub / mirror (push) Successful in 26s
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m28s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Has been skipped

Original: feat: enhance security logging and ensure rate limit blocks are properly tracked

- Add comprehensive logging for rate limit blocks with full request details
  - IP address (with proper proxy detection), user agent, headers, timestamps
  - Rate limit info (current count, limit, remaining, reset time)
  - Separate tracking for auth vs general endpoints

- Enhance authentication failure logging
  - JWT validation failures with detailed error info
  - Admin auth attempts without token
  - Failed token validation with user context
  - All events include IP, path, method, user agent

- Improve Winston logger configuration for production
  - Add automatic log rotation (10MB errors, 50MB combined)
  - Create separate security.log for auth/rate limit events
  - Ensure logs directory exists automatically
  - Add structured JSON format for log aggregation
  - Support container logging with LOG_TO_CONSOLE env var

- Create comprehensive documentation
  - Security logging guide with examples
  - Monitoring recommendations
  - Configuration reference

- Add test script to verify logging functionality

All rate limit settings remain configurable via admin panel:
- Window duration, max requests, auth limits
- Skip authenticated requests option
- Public endpoints only option

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-18 19:25:15 +02:00
commit 1773ed5f95
354 changed files with 61508 additions and 0 deletions
@@ -0,0 +1,187 @@
import React, { useState, useEffect, useRef } from 'react';
import { ChevronLeft, ChevronRight, Download, Maximize2, Play, Pause } from 'lucide-react';
import { useTheme } from '../../../contexts/ThemeContext';
import { AuthenticatedImage, Button } from '../../common';
import type { BaseGalleryLayoutProps } from './BaseGalleryLayout';
export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
photos,
onPhotoClick,
onDownload,
// selectedPhotos = new Set(),
// isSelectionMode = false
}) => {
const { theme } = useTheme();
const [currentIndex, setCurrentIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const gallerySettings = theme.gallerySettings || {};
const autoplay = gallerySettings.carouselAutoplay || false;
const interval = gallerySettings.carouselInterval || 5000;
const showThumbnails = gallerySettings.carouselShowThumbnails !== false;
// Auto-play functionality
useEffect(() => {
if (isPlaying && photos.length > 1) {
intervalRef.current = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % photos.length);
}, interval);
} else if (intervalRef.current) {
clearInterval(intervalRef.current);
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [isPlaying, photos.length, interval]);
// Start autoplay if enabled
useEffect(() => {
if (autoplay) {
setIsPlaying(true);
}
}, [autoplay]);
const goToPrevious = () => {
setCurrentIndex((prev) => (prev - 1 + photos.length) % photos.length);
};
const goToNext = () => {
setCurrentIndex((prev) => (prev + 1) % photos.length);
};
const togglePlayPause = () => {
setIsPlaying(!isPlaying);
};
if (photos.length === 0) return null;
const currentPhoto = photos[currentIndex];
return (
<div className="relative">
{/* Main Carousel */}
<div className="relative h-[50vh] sm:h-[60vh] lg:h-[70vh] bg-black rounded-lg overflow-hidden">
<AuthenticatedImage
src={currentPhoto.url}
alt={currentPhoto.filename}
className="w-full h-full object-contain"
isGallery={true}
/>
{/* Navigation Controls */}
<div className="absolute inset-0 flex items-center justify-between p-4">
<button
onClick={goToPrevious}
className="p-2 bg-black/50 text-white rounded-full hover:bg-black/70 transition-colors"
aria-label="Previous photo"
>
<ChevronLeft className="w-6 h-6" />
</button>
<button
onClick={goToNext}
className="p-2 bg-black/50 text-white rounded-full hover:bg-black/70 transition-colors"
aria-label="Next photo"
>
<ChevronRight className="w-6 h-6" />
</button>
</div>
{/* Top Controls */}
<div className="absolute top-4 left-4 right-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="px-3 py-1 bg-black/50 text-white rounded-full text-sm">
{currentIndex + 1} / {photos.length}
</span>
{currentPhoto.category_name && (
<span className="px-3 py-1 bg-black/50 text-white rounded-full text-sm">
{currentPhoto.category_name}
</span>
)}
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={togglePlayPause}
className="text-white hover:bg-white/20"
title={isPlaying ? 'Pause slideshow' : 'Play slideshow'}
>
{isPlaying ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onPhotoClick(currentIndex)}
className="text-white hover:bg-white/20"
title="View fullscreen"
>
<Maximize2 className="w-5 h-5" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => onDownload(currentPhoto, e)}
className="text-white hover:bg-white/20"
title="Download photo"
>
<Download className="w-5 h-5" />
</Button>
</div>
</div>
{/* Progress Bar */}
{isPlaying && (
<div className="absolute bottom-0 left-0 right-0 h-1 bg-white/20">
<div
className="h-full bg-white transition-all duration-1000 ease-linear"
style={{
width: '100%',
animation: `progress ${interval}ms linear infinite`
}}
/>
</div>
)}
</div>
{/* Thumbnails */}
{showThumbnails && photos.length > 1 && (
<div className="mt-4 relative">
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-thin scrollbar-thumb-neutral-400">
{photos.map((photo, index) => (
<button
key={photo.id}
onClick={() => setCurrentIndex(index)}
className={`relative flex-shrink-0 w-20 h-20 rounded overflow-hidden transition-all ${
index === currentIndex
? 'ring-2 ring-primary-600 scale-110'
: 'opacity-70 hover:opacity-100'
}`}
>
<AuthenticatedImage
src={photo.thumbnail_url || photo.url}
alt={photo.filename}
className="w-full h-full object-cover"
loading="lazy"
isGallery={true}
/>
</button>
))}
</div>
</div>
)}
<style>{`
@keyframes progress {
from { width: 0%; }
to { width: 100%; }
}
`}</style>
</div>
);
};