Merge pull request #1303 from PicPeak/fix/1300-remove-fragmentation
fix: remove the image-fragmentation surface
This commit is contained in:
@@ -1597,7 +1597,6 @@ module.exports = (router) => {
|
|||||||
body('use_canvas_rendering').optional().isBoolean(),
|
body('use_canvas_rendering').optional().isBoolean(),
|
||||||
body('overlay_protection').optional().isBoolean(),
|
body('overlay_protection').optional().isBoolean(),
|
||||||
body('image_quality').optional().isInt({ min: 1, max: 100 }),
|
body('image_quality').optional().isInt({ min: 1, max: 100 }),
|
||||||
body('fragmentation_level').optional().isInt({ min: 1, max: 10 }),
|
|
||||||
body('password').optional().isString().custom((value) => {
|
body('password').optional().isString().custom((value) => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '') {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ router.get('/settings', adminAuth, requirePermission(['settings.view', 'image_se
|
|||||||
'max_image_requests_per_hour',
|
'max_image_requests_per_hour',
|
||||||
'suspicious_activity_threshold',
|
'suspicious_activity_threshold',
|
||||||
'enable_canvas_rendering',
|
'enable_canvas_rendering',
|
||||||
'default_fragmentation_level',
|
|
||||||
'security_monitoring_enabled',
|
'security_monitoring_enabled',
|
||||||
'block_suspicious_ips',
|
'block_suspicious_ips',
|
||||||
'log_security_events_to_db',
|
'log_security_events_to_db',
|
||||||
@@ -68,7 +67,6 @@ router.put('/settings', adminAuth, requirePermission('image_security.manage'), a
|
|||||||
'max_image_requests_per_hour',
|
'max_image_requests_per_hour',
|
||||||
'suspicious_activity_threshold',
|
'suspicious_activity_threshold',
|
||||||
'enable_canvas_rendering',
|
'enable_canvas_rendering',
|
||||||
'default_fragmentation_level',
|
|
||||||
'security_monitoring_enabled',
|
'security_monitoring_enabled',
|
||||||
'block_suspicious_ips',
|
'block_suspicious_ips',
|
||||||
'log_security_events_to_db',
|
'log_security_events_to_db',
|
||||||
|
|||||||
@@ -1227,7 +1227,6 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, noStoreCache, asy
|
|||||||
protection_level: req.event.protection_level || 'standard',
|
protection_level: req.event.protection_level || 'standard',
|
||||||
image_quality: req.event.image_quality || 85,
|
image_quality: req.event.image_quality || 85,
|
||||||
use_canvas_rendering: parseBooleanInput(req.event.use_canvas_rendering, false),
|
use_canvas_rendering: parseBooleanInput(req.event.use_canvas_rendering, false),
|
||||||
fragmentation_level: req.event.fragmentation_level || 3,
|
|
||||||
overlay_protection: parseBooleanInput(req.event.overlay_protection, true)
|
overlay_protection: parseBooleanInput(req.event.overlay_protection, true)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -117,8 +117,7 @@ router.get('/:slug/photo/:photoId/view', verifyGalleryAccess, blockHiddenGallery
|
|||||||
const protectionSettings = {
|
const protectionSettings = {
|
||||||
protectionLevel: eventProtectionLevel,
|
protectionLevel: eventProtectionLevel,
|
||||||
quality: req.event.image_quality || 85,
|
quality: req.event.image_quality || 85,
|
||||||
addFingerprint: req.event.add_fingerprint !== false,
|
addFingerprint: req.event.add_fingerprint !== false
|
||||||
fragmentImage: eventProtectionLevel === 'maximum'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resolve photo location through the storage backend (managed) or local
|
// Resolve photo location through the storage backend (managed) or local
|
||||||
@@ -151,21 +150,6 @@ router.get('/:slug/photo/:photoId/view', verifyGalleryAccess, blockHiddenGallery
|
|||||||
? await withLocalCopy(storageKey, runProcessing)
|
? await withLocalCopy(storageKey, runProcessing)
|
||||||
: await runProcessing(resolvePhotoFilePath(req.event, photo));
|
: await runProcessing(resolvePhotoFilePath(req.event, photo));
|
||||||
|
|
||||||
if (processedImage.type === 'fragmented') {
|
|
||||||
return res.json({
|
|
||||||
type: 'fragmented',
|
|
||||||
fragments: processedImage.fragments.map(f => ({
|
|
||||||
index: f.index,
|
|
||||||
row: f.row,
|
|
||||||
col: f.col,
|
|
||||||
data: f.buffer.toString('base64'),
|
|
||||||
position: f.position
|
|
||||||
})),
|
|
||||||
dimensions: processedImage.originalDimensions,
|
|
||||||
fragmentDimensions: processedImage.fragmentDimensions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
finalImage = processedImage;
|
finalImage = processedImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,8 +120,6 @@ router.get('/:slug/secure/:photoId/:token',
|
|||||||
tokenLength: token?.length,
|
tokenLength: token?.length,
|
||||||
hasAuthHeader: Boolean(req.headers.authorization),
|
hasAuthHeader: Boolean(req.headers.authorization),
|
||||||
});
|
});
|
||||||
const { fragment } = req.query;
|
|
||||||
|
|
||||||
// Verify secure token
|
// Verify secure token
|
||||||
const tokenValidation = secureImageService.verifySecureToken(
|
const tokenValidation = secureImageService.verifySecureToken(
|
||||||
token,
|
token,
|
||||||
@@ -212,8 +210,7 @@ router.get('/:slug/secure/:photoId/:token',
|
|||||||
const protectionSettings = {
|
const protectionSettings = {
|
||||||
protectionLevel: event.protection_level || 'standard',
|
protectionLevel: event.protection_level || 'standard',
|
||||||
quality: event.image_quality || 85,
|
quality: event.image_quality || 85,
|
||||||
addFingerprint: event.add_fingerprint !== false,
|
addFingerprint: event.add_fingerprint !== false
|
||||||
fragmentImage: event.use_canvas_rendering === true && fragment !== undefined
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let processedImage;
|
let processedImage;
|
||||||
@@ -232,11 +229,6 @@ router.get('/:slug/secure/:photoId/:token',
|
|||||||
return res.status(404).json({ error: 'Photo file not found' });
|
return res.status(404).json({ error: 'Photo file not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle fragmented images
|
|
||||||
if (processedImage.type === 'fragmented') {
|
|
||||||
return await handleFragmentedImage(req, res, processedImage, fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log successful access
|
// Log successful access
|
||||||
await secureImageService.logImageAccess(
|
await secureImageService.logImageAccess(
|
||||||
photoId,
|
photoId,
|
||||||
@@ -267,58 +259,6 @@ router.get('/:slug/secure/:photoId/:token',
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle fragmented image delivery
|
|
||||||
*/
|
|
||||||
async function handleFragmentedImage(req, res, fragmentedImage, fragmentIndex) {
|
|
||||||
const { photoId } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (fragmentIndex === undefined) {
|
|
||||||
// Return fragment metadata
|
|
||||||
res.json({
|
|
||||||
type: 'fragmented',
|
|
||||||
fragments: fragmentedImage.fragments.length,
|
|
||||||
dimensions: fragmentedImage.originalDimensions,
|
|
||||||
fragmentDimensions: fragmentedImage.fragmentDimensions
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = parseInt(fragmentIndex);
|
|
||||||
if (isNaN(index) || index < 0 || index >= fragmentedImage.fragments.length) {
|
|
||||||
return res.status(400).json({ error: 'Invalid fragment index' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const fragment = fragmentedImage.fragments[index];
|
|
||||||
|
|
||||||
// Log fragment access
|
|
||||||
await secureImageService.logImageAccess(
|
|
||||||
photoId,
|
|
||||||
req.event.id,
|
|
||||||
req.clientInfo,
|
|
||||||
`fragment_${index}`
|
|
||||||
);
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'image/jpeg',
|
|
||||||
'Content-Length': fragment.buffer.length,
|
|
||||||
'X-Fragment-Index': index,
|
|
||||||
'X-Fragment-Position': JSON.stringify(fragment.position)
|
|
||||||
});
|
|
||||||
|
|
||||||
res.send(fragment.buffer);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error serving image fragment', {
|
|
||||||
error: error.message,
|
|
||||||
fragmentIndex,
|
|
||||||
photoId
|
|
||||||
});
|
|
||||||
res.status(500).json({ error: 'Failed to serve image fragment' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download protected image with watermark
|
* Download protected image with watermark
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -175,8 +175,7 @@ class SecureImageService {
|
|||||||
quality = 85,
|
quality = 85,
|
||||||
maxWidth = 1920,
|
maxWidth = 1920,
|
||||||
maxHeight = 1080,
|
maxHeight = 1080,
|
||||||
addFingerprint = true,
|
addFingerprint = true
|
||||||
fragmentImage = false
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -187,7 +186,7 @@ class SecureImageService {
|
|||||||
|
|
||||||
// For standard protection without fingerprinting, return original file
|
// For standard protection without fingerprinting, return original file
|
||||||
// This avoids unnecessary recompression when no protection features are needed
|
// This avoids unnecessary recompression when no protection features are needed
|
||||||
if (protectionLevel === 'standard' && !addFingerprint && !fragmentImage) {
|
if (protectionLevel === 'standard' && !addFingerprint) {
|
||||||
return await fs.readFile(imagePath);
|
return await fs.readFile(imagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,14 +267,7 @@ class SecureImageService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = await image.toBuffer();
|
return await image.toBuffer();
|
||||||
|
|
||||||
// Fragment image if requested (for canvas reconstruction)
|
|
||||||
if (fragmentImage && protectionLevel === 'maximum') {
|
|
||||||
return await this.fragmentImageBuffer(buffer, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error processing protected image:', error);
|
logger.error('Error processing protected image:', error);
|
||||||
// Return original on error
|
// Return original on error
|
||||||
@@ -283,51 +275,6 @@ class SecureImageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment image into multiple pieces for canvas reconstruction
|
|
||||||
*/
|
|
||||||
async fragmentImageBuffer(buffer, metadata) {
|
|
||||||
const { width, height } = metadata;
|
|
||||||
const fragments = [];
|
|
||||||
|
|
||||||
// Create 3x3 grid of fragments
|
|
||||||
const cols = 3;
|
|
||||||
const rows = 3;
|
|
||||||
const fragmentWidth = Math.floor(width / cols);
|
|
||||||
const fragmentHeight = Math.floor(height / rows);
|
|
||||||
|
|
||||||
for (let row = 0; row < rows; row++) {
|
|
||||||
for (let col = 0; col < cols; col++) {
|
|
||||||
const left = col * fragmentWidth;
|
|
||||||
const top = row * fragmentHeight;
|
|
||||||
|
|
||||||
const fragment = await sharp(buffer)
|
|
||||||
.extract({
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
width: fragmentWidth,
|
|
||||||
height: fragmentHeight
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
fragments.push({
|
|
||||||
index: row * cols + col,
|
|
||||||
row,
|
|
||||||
col,
|
|
||||||
buffer: fragment,
|
|
||||||
position: { left, top, width: fragmentWidth, height: fragmentHeight }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'fragmented',
|
|
||||||
fragments,
|
|
||||||
originalDimensions: { width, height },
|
|
||||||
fragmentDimensions: { width: fragmentWidth, height: fragmentHeight, cols, rows }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log image access for security monitoring
|
* Log image access for security monitoring
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ interface ProtectedImageProps extends React.CanvasHTMLAttributes<HTMLCanvasEleme
|
|||||||
alt: string;
|
alt: string;
|
||||||
protectionLevel?: ProtectionLevel;
|
protectionLevel?: ProtectionLevel;
|
||||||
watermarkText?: string;
|
watermarkText?: string;
|
||||||
fragmentGrid?: boolean;
|
|
||||||
gridSize?: number;
|
|
||||||
scrambleFragments?: boolean;
|
|
||||||
invisibleWatermark?: boolean;
|
invisibleWatermark?: boolean;
|
||||||
onProtectionViolation?: (violationType: string) => void;
|
onProtectionViolation?: (violationType: string) => void;
|
||||||
fallbackSrc?: string;
|
fallbackSrc?: string;
|
||||||
@@ -26,9 +23,6 @@ export const ProtectedImage: React.FC<ProtectedImageProps> = ({
|
|||||||
alt,
|
alt,
|
||||||
protectionLevel = 'standard',
|
protectionLevel = 'standard',
|
||||||
watermarkText,
|
watermarkText,
|
||||||
fragmentGrid = false,
|
|
||||||
gridSize = 4,
|
|
||||||
scrambleFragments = false,
|
|
||||||
invisibleWatermark = false,
|
invisibleWatermark = false,
|
||||||
onProtectionViolation,
|
onProtectionViolation,
|
||||||
fallbackSrc,
|
fallbackSrc,
|
||||||
@@ -120,52 +114,6 @@ export const ProtectedImage: React.FC<ProtectedImageProps> = ({
|
|||||||
ctx.shadowOffsetY = 0;
|
ctx.shadowOffsetY = 0;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Fragment and scramble image for maximum protection
|
|
||||||
const renderFragmentedImage = useCallback((
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
img: HTMLImageElement,
|
|
||||||
width: number,
|
|
||||||
height: number
|
|
||||||
) => {
|
|
||||||
const fragmentWidth = width / gridSize;
|
|
||||||
const fragmentHeight = height / gridSize;
|
|
||||||
const fragments: Array<{ x: number; y: number; destX: number; destY: number }> = [];
|
|
||||||
|
|
||||||
// Create fragment map
|
|
||||||
for (let row = 0; row < gridSize; row++) {
|
|
||||||
for (let col = 0; col < gridSize; col++) {
|
|
||||||
fragments.push({
|
|
||||||
x: col * fragmentWidth,
|
|
||||||
y: row * fragmentHeight,
|
|
||||||
destX: col * fragmentWidth,
|
|
||||||
destY: row * fragmentHeight,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scramble fragments if requested
|
|
||||||
if (scrambleFragments) {
|
|
||||||
for (let i = fragments.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
const temp = fragments[i].destX;
|
|
||||||
const tempY = fragments[i].destY;
|
|
||||||
fragments[i].destX = fragments[j].destX;
|
|
||||||
fragments[i].destY = fragments[j].destY;
|
|
||||||
fragments[j].destX = temp;
|
|
||||||
fragments[j].destY = tempY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw fragments
|
|
||||||
fragments.forEach(fragment => {
|
|
||||||
ctx.drawImage(
|
|
||||||
img,
|
|
||||||
fragment.x, fragment.y, fragmentWidth, fragmentHeight,
|
|
||||||
fragment.destX, fragment.destY, fragmentWidth, fragmentHeight
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [gridSize, scrambleFragments]);
|
|
||||||
|
|
||||||
// Main canvas rendering function - wrapped in useCallback to prevent infinite re-renders
|
// Main canvas rendering function - wrapped in useCallback to prevent infinite re-renders
|
||||||
const renderToCanvas = useCallback(() => {
|
const renderToCanvas = useCallback(() => {
|
||||||
if (!canvasRef.current || !imageRef.current) {
|
if (!canvasRef.current || !imageRef.current) {
|
||||||
@@ -199,14 +147,9 @@ export const ProtectedImage: React.FC<ProtectedImageProps> = ({
|
|||||||
ctx.globalCompositeOperation = 'source-over'; // Reset composite operation
|
ctx.globalCompositeOperation = 'source-over'; // Reset composite operation
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (fragmentGrid && (protectionLevel === 'enhanced' || protectionLevel === 'maximum')) {
|
// Ensure image is valid before drawing
|
||||||
// Render fragmented image
|
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
|
||||||
renderFragmentedImage(ctx, img, canvas.width, canvas.height);
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
} else {
|
|
||||||
// Render normal image - ensure image is valid before drawing
|
|
||||||
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
|
|
||||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply watermarks
|
// Apply watermarks
|
||||||
@@ -242,7 +185,7 @@ export const ProtectedImage: React.FC<ProtectedImageProps> = ({
|
|||||||
reportViolation('canvas_rendering_error');
|
reportViolation('canvas_rendering_error');
|
||||||
setError(true);
|
setError(true);
|
||||||
}
|
}
|
||||||
}, [fragmentGrid, protectionLevel, renderFragmentedImage, watermarkText, invisibleWatermark, applyInvisibleWatermark, applyVisibleWatermark, reportViolation]);
|
}, [protectionLevel, watermarkText, invisibleWatermark, applyInvisibleWatermark, applyVisibleWatermark, reportViolation]);
|
||||||
|
|
||||||
// Set up protection event listeners
|
// Set up protection event listeners
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -128,25 +128,6 @@ describe('ProtectedImage', () => {
|
|||||||
expect(mockContext.fillText).toHaveBeenCalled();
|
expect(mockContext.fillText).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles fragment grid rendering', async () => {
|
|
||||||
render(
|
|
||||||
<ProtectedImage
|
|
||||||
{...defaultProps}
|
|
||||||
fragmentGrid={true}
|
|
||||||
gridSize={4}
|
|
||||||
protectionLevel="enhanced"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const canvas = screen.getByRole('img', { name: 'Test image' });
|
|
||||||
expect(canvas).toHaveStyle({ opacity: '1' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify multiple drawImage calls for fragments
|
|
||||||
expect(mockContext.drawImage).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('blocks interactions in maximum protection mode', async () => {
|
it('blocks interactions in maximum protection mode', async () => {
|
||||||
const onViolation = vi.fn();
|
const onViolation = vi.fn();
|
||||||
|
|
||||||
@@ -230,25 +211,6 @@ describe('ProtectedImage', () => {
|
|||||||
expect(mockContext.putImageData).toHaveBeenCalled();
|
expect(mockContext.putImageData).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('scrambles fragments when enabled', async () => {
|
|
||||||
render(
|
|
||||||
<ProtectedImage
|
|
||||||
{...defaultProps}
|
|
||||||
fragmentGrid={true}
|
|
||||||
scrambleFragments={true}
|
|
||||||
protectionLevel="maximum"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const canvas = screen.getByRole('img', { name: 'Test image' });
|
|
||||||
expect(canvas).toHaveStyle({ opacity: '1' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fragment scrambling should result in multiple drawImage calls
|
|
||||||
expect(mockContext.drawImage).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds random noise in maximum protection', async () => {
|
it('adds random noise in maximum protection', async () => {
|
||||||
render(
|
render(
|
||||||
<ProtectedImage
|
<ProtectedImage
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ interface ImageSecuritySettings {
|
|||||||
max_image_requests_per_hour: number;
|
max_image_requests_per_hour: number;
|
||||||
suspicious_activity_threshold: number;
|
suspicious_activity_threshold: number;
|
||||||
enable_canvas_rendering: boolean;
|
enable_canvas_rendering: boolean;
|
||||||
default_fragmentation_level: number;
|
|
||||||
security_monitoring_enabled: boolean;
|
security_monitoring_enabled: boolean;
|
||||||
block_suspicious_ips: boolean;
|
block_suspicious_ips: boolean;
|
||||||
log_security_events_to_db: boolean;
|
log_security_events_to_db: boolean;
|
||||||
@@ -31,7 +30,6 @@ const defaultSettings: ImageSecuritySettings = {
|
|||||||
max_image_requests_per_hour: 500,
|
max_image_requests_per_hour: 500,
|
||||||
suspicious_activity_threshold: 10,
|
suspicious_activity_threshold: 10,
|
||||||
enable_canvas_rendering: false,
|
enable_canvas_rendering: false,
|
||||||
default_fragmentation_level: 3,
|
|
||||||
security_monitoring_enabled: true,
|
security_monitoring_enabled: true,
|
||||||
block_suspicious_ips: true,
|
block_suspicious_ips: true,
|
||||||
log_security_events_to_db: true,
|
log_security_events_to_db: true,
|
||||||
@@ -161,21 +159,6 @@ export const ImageSecurityTab: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{t('settings.imageSecurity.imageQualityHelp', '1-100, higher = better quality')}</p>
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{t('settings.imageSecurity.imageQualityHelp', '1-100, higher = better quality')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
|
||||||
{t('settings.imageSecurity.fragmentationLevel', 'Fragmentation Level')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="10"
|
|
||||||
value={settings.default_fragmentation_level}
|
|
||||||
onChange={(e) => handleChange('default_fragmentation_level', parseInt(e.target.value) || 3)}
|
|
||||||
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{t('settings.imageSecurity.fragmentationLevelHelp', '1-10, higher = more protection')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ interface UseImageProtectionOptions {
|
|||||||
blockKeyboardShortcuts?: boolean;
|
blockKeyboardShortcuts?: boolean;
|
||||||
detectPrintScreen?: boolean;
|
detectPrintScreen?: boolean;
|
||||||
watermarkText?: string;
|
watermarkText?: string;
|
||||||
fragmentGrid?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useImageProtection = (options: UseImageProtectionOptions) => {
|
export const useImageProtection = (options: UseImageProtectionOptions) => {
|
||||||
|
|||||||
@@ -1852,7 +1852,6 @@
|
|||||||
"defaultProtectionHelp": "Diese Einstellungen gelten für alle neuen Veranstaltungen. Einzelne Veranstaltungen können diese Standardwerte überschreiben.",
|
"defaultProtectionHelp": "Diese Einstellungen gelten für alle neuen Veranstaltungen. Einzelne Veranstaltungen können diese Standardwerte überschreiben.",
|
||||||
"protectionLevel": "Standard-Schutzstufe",
|
"protectionLevel": "Standard-Schutzstufe",
|
||||||
"imageQuality": "Standard-Bildqualität",
|
"imageQuality": "Standard-Bildqualität",
|
||||||
"fragmentationLevel": "Fragmentierungsstufe",
|
|
||||||
"enableDevtools": "DevTools-Erkennung standardmäßig aktivieren",
|
"enableDevtools": "DevTools-Erkennung standardmäßig aktivieren",
|
||||||
"enableCanvas": "Canvas-Rendering standardmäßig aktivieren (erweiterter Schutz)",
|
"enableCanvas": "Canvas-Rendering standardmäßig aktivieren (erweiterter Schutz)",
|
||||||
"rateLimiting": "Ratenbegrenzung",
|
"rateLimiting": "Ratenbegrenzung",
|
||||||
@@ -1869,7 +1868,6 @@
|
|||||||
"infoTitle": "Über Bildschutz",
|
"infoTitle": "Über Bildschutz",
|
||||||
"infoText": "Diese Schutzfunktionen helfen, gelegentliches Herunterladen und Kopieren zu verhindern, können aber nicht alle Methoden blockieren. Entschlossene Benutzer finden möglicherweise trotzdem Wege, Bilder zu erfassen. Erwägen Sie die Verwendung von Wasserzeichen und rechtlichen Vereinbarungen für umfassenden Schutz.",
|
"infoText": "Diese Schutzfunktionen helfen, gelegentliches Herunterladen und Kopieren zu verhindern, können aber nicht alle Methoden blockieren. Entschlossene Benutzer finden möglicherweise trotzdem Wege, Bilder zu erfassen. Erwägen Sie die Verwendung von Wasserzeichen und rechtlichen Vereinbarungen für umfassenden Schutz.",
|
||||||
"imageQualityHelp": "1–100, höher = bessere Qualität",
|
"imageQualityHelp": "1–100, höher = bessere Qualität",
|
||||||
"fragmentationLevelHelp": "1–10, höher = mehr Schutz",
|
|
||||||
"suspiciousActivityThresholdHelp": "Verstöße, bevor als verdächtig markiert wird",
|
"suspiciousActivityThresholdHelp": "Verstöße, bevor als verdächtig markiert wird",
|
||||||
"autoBlockThresholdHelp": "Verstöße, bevor die IP automatisch gesperrt wird"
|
"autoBlockThresholdHelp": "Verstöße, bevor die IP automatisch gesperrt wird"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1415,7 +1415,6 @@
|
|||||||
"defaultProtectionHelp": "These settings apply to all new events. Individual events can override these defaults.",
|
"defaultProtectionHelp": "These settings apply to all new events. Individual events can override these defaults.",
|
||||||
"protectionLevel": "Default Protection Level",
|
"protectionLevel": "Default Protection Level",
|
||||||
"imageQuality": "Default Image Quality",
|
"imageQuality": "Default Image Quality",
|
||||||
"fragmentationLevel": "Fragmentation Level",
|
|
||||||
"enableDevtools": "Enable DevTools detection by default",
|
"enableDevtools": "Enable DevTools detection by default",
|
||||||
"enableCanvas": "Enable canvas rendering by default (advanced protection)",
|
"enableCanvas": "Enable canvas rendering by default (advanced protection)",
|
||||||
"rateLimiting": "Rate Limiting",
|
"rateLimiting": "Rate Limiting",
|
||||||
@@ -1432,7 +1431,6 @@
|
|||||||
"infoTitle": "About Image Protection",
|
"infoTitle": "About Image Protection",
|
||||||
"infoText": "These protection features help prevent casual downloading and copying but cannot block all methods. Determined users may still find ways to capture images. Consider using watermarks and legal agreements for comprehensive protection.",
|
"infoText": "These protection features help prevent casual downloading and copying but cannot block all methods. Determined users may still find ways to capture images. Consider using watermarks and legal agreements for comprehensive protection.",
|
||||||
"imageQualityHelp": "1-100, higher = better quality",
|
"imageQualityHelp": "1-100, higher = better quality",
|
||||||
"fragmentationLevelHelp": "1-10, higher = more protection",
|
|
||||||
"suspiciousActivityThresholdHelp": "Violations before flagging as suspicious",
|
"suspiciousActivityThresholdHelp": "Violations before flagging as suspicious",
|
||||||
"autoBlockThresholdHelp": "Violations before auto-blocking IP"
|
"autoBlockThresholdHelp": "Violations before auto-blocking IP"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -941,7 +941,6 @@
|
|||||||
"defaultProtectionHelp": "Estos ajustes se aplican a todos los nuevos eventos. Los eventos individuales pueden anular estos valores por defecto.",
|
"defaultProtectionHelp": "Estos ajustes se aplican a todos los nuevos eventos. Los eventos individuales pueden anular estos valores por defecto.",
|
||||||
"protectionLevel": "Nivel de protección por defecto",
|
"protectionLevel": "Nivel de protección por defecto",
|
||||||
"imageQuality": "Calidad de imagen por defecto",
|
"imageQuality": "Calidad de imagen por defecto",
|
||||||
"fragmentationLevel": "Nivel de fragmentación",
|
|
||||||
"enableDevtools": "Habilitar detección de DevTools por defecto",
|
"enableDevtools": "Habilitar detección de DevTools por defecto",
|
||||||
"enableCanvas": "Habilitar renderizado con canvas por defecto (protección avanzada)",
|
"enableCanvas": "Habilitar renderizado con canvas por defecto (protección avanzada)",
|
||||||
"rateLimiting": "Limitación de velocidad",
|
"rateLimiting": "Limitación de velocidad",
|
||||||
|
|||||||
@@ -911,7 +911,6 @@
|
|||||||
"defaultProtectionHelp": "Ces paramètres s'appliquent à tous les nouveaux événements. Les événements individuels peuvent remplacer ces valeurs par défaut.",
|
"defaultProtectionHelp": "Ces paramètres s'appliquent à tous les nouveaux événements. Les événements individuels peuvent remplacer ces valeurs par défaut.",
|
||||||
"protectionLevel": "Niveau de protection par défaut",
|
"protectionLevel": "Niveau de protection par défaut",
|
||||||
"imageQuality": "Qualité d'image par défaut",
|
"imageQuality": "Qualité d'image par défaut",
|
||||||
"fragmentationLevel": "Niveau de fragmentation",
|
|
||||||
"enableDevtools": "Activer la détection des outils DevTools par défaut",
|
"enableDevtools": "Activer la détection des outils DevTools par défaut",
|
||||||
"enableCanvas": "Activer le rendu canvas par défaut (protection avancée)",
|
"enableCanvas": "Activer le rendu canvas par défaut (protection avancée)",
|
||||||
"rateLimiting": "Limitation de débit",
|
"rateLimiting": "Limitation de débit",
|
||||||
|
|||||||
@@ -907,7 +907,6 @@
|
|||||||
"defaultProtectionHelp": "Deze instellingen zijn van toepassing op alle nieuwe evenementen. Individuele evenementen kunnen deze standaardwaarden overschrijven.",
|
"defaultProtectionHelp": "Deze instellingen zijn van toepassing op alle nieuwe evenementen. Individuele evenementen kunnen deze standaardwaarden overschrijven.",
|
||||||
"protectionLevel": "Standaard beveiligingsniveau",
|
"protectionLevel": "Standaard beveiligingsniveau",
|
||||||
"imageQuality": "Standaard beeldkwaliteit",
|
"imageQuality": "Standaard beeldkwaliteit",
|
||||||
"fragmentationLevel": "Fragmentatieniveau",
|
|
||||||
"enableDevtools": "Standaard DevTools-detectie inschakelen",
|
"enableDevtools": "Standaard DevTools-detectie inschakelen",
|
||||||
"enableCanvas": "Standaard canvas-rendering inschakelen (geavanceerde beveiliging)",
|
"enableCanvas": "Standaard canvas-rendering inschakelen (geavanceerde beveiliging)",
|
||||||
"rateLimiting": "Snelheidsbeperking",
|
"rateLimiting": "Snelheidsbeperking",
|
||||||
|
|||||||
@@ -924,7 +924,6 @@
|
|||||||
"defaultProtectionHelp": "Aplicado a novos eventos por padrão. Podem ser substituídas individualmente.",
|
"defaultProtectionHelp": "Aplicado a novos eventos por padrão. Podem ser substituídas individualmente.",
|
||||||
"protectionLevel": "Nível de Proteção Padrão",
|
"protectionLevel": "Nível de Proteção Padrão",
|
||||||
"imageQuality": "Qualidade de Imagem Padrão",
|
"imageQuality": "Qualidade de Imagem Padrão",
|
||||||
"fragmentationLevel": "Nível de Fragmentação",
|
|
||||||
"enableDevtools": "Ativar detecção de DevTools por padrão",
|
"enableDevtools": "Ativar detecção de DevTools por padrão",
|
||||||
"enableCanvas": "Ativar renderização em Canvas por padrão",
|
"enableCanvas": "Ativar renderização em Canvas por padrão",
|
||||||
"rateLimiting": "Limite de Requisições",
|
"rateLimiting": "Limite de Requisições",
|
||||||
|
|||||||
@@ -930,7 +930,6 @@
|
|||||||
"defaultProtectionHelp": "Эти настройки применяются ко всем новым событиям. Отдельные события могут их переопределить.",
|
"defaultProtectionHelp": "Эти настройки применяются ко всем новым событиям. Отдельные события могут их переопределить.",
|
||||||
"protectionLevel": "Уровень защиты по умолчанию",
|
"protectionLevel": "Уровень защиты по умолчанию",
|
||||||
"imageQuality": "Качество изображений по умолчанию",
|
"imageQuality": "Качество изображений по умолчанию",
|
||||||
"fragmentationLevel": "Уровень фрагментации",
|
|
||||||
"enableDevtools": "Включить обнаружение DevTools по умолчанию",
|
"enableDevtools": "Включить обнаружение DevTools по умолчанию",
|
||||||
"enableCanvas": "Включить рендеринг Canvas по умолчанию (расширенная защита)",
|
"enableCanvas": "Включить рендеринг Canvas по умолчанию (расширенная защита)",
|
||||||
"rateLimiting": "Ограничение скорости",
|
"rateLimiting": "Ограничение скорости",
|
||||||
|
|||||||
@@ -911,7 +911,6 @@
|
|||||||
"defaultProtectionHelp": "Te nastavitve veljajo za vse nove dogodke. Posamezni dogodki lahko te privzete nastavitve prepišejo.",
|
"defaultProtectionHelp": "Te nastavitve veljajo za vse nove dogodke. Posamezni dogodki lahko te privzete nastavitve prepišejo.",
|
||||||
"protectionLevel": "Privzeta raven zaščite",
|
"protectionLevel": "Privzeta raven zaščite",
|
||||||
"imageQuality": "Privzeta kakovost slike",
|
"imageQuality": "Privzeta kakovost slike",
|
||||||
"fragmentationLevel": "Raven fragmentacije",
|
|
||||||
"enableDevtools": "Privzeto omogoči zaznavanje DevTools",
|
"enableDevtools": "Privzeto omogoči zaznavanje DevTools",
|
||||||
"enableCanvas": "Privzeto omogoči izris prek canvas (napredna zaščita)",
|
"enableCanvas": "Privzeto omogoči izris prek canvas (napredna zaščita)",
|
||||||
"rateLimiting": "Omejevanje hitrosti",
|
"rateLimiting": "Omejevanje hitrosti",
|
||||||
|
|||||||
@@ -290,7 +290,6 @@ export interface GalleryData {
|
|||||||
image_quality?: number;
|
image_quality?: number;
|
||||||
use_canvas_rendering?: boolean;
|
use_canvas_rendering?: boolean;
|
||||||
enable_devtools_protection?: boolean;
|
enable_devtools_protection?: boolean;
|
||||||
fragmentation_level?: number;
|
|
||||||
overlay_protection?: boolean;
|
overlay_protection?: boolean;
|
||||||
// Hero logo customization fields
|
// Hero logo customization fields
|
||||||
hero_logo_visible?: boolean | null;
|
hero_logo_visible?: boolean | null;
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export interface ImageProtectionOptions {
|
|||||||
blockKeyboardShortcuts?: boolean;
|
blockKeyboardShortcuts?: boolean;
|
||||||
detectPrintScreen?: boolean;
|
detectPrintScreen?: boolean;
|
||||||
watermarkText?: string;
|
watermarkText?: string;
|
||||||
fragmentGrid?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProtectedImageProps {
|
export interface ProtectedImageProps {
|
||||||
@@ -57,9 +56,6 @@ export interface ProtectedImageProps {
|
|||||||
alt: string;
|
alt: string;
|
||||||
protectionLevel?: ProtectionLevel;
|
protectionLevel?: ProtectionLevel;
|
||||||
watermarkText?: string;
|
watermarkText?: string;
|
||||||
fragmentGrid?: boolean;
|
|
||||||
gridSize?: number;
|
|
||||||
scrambleFragments?: boolean;
|
|
||||||
invisibleWatermark?: boolean;
|
invisibleWatermark?: boolean;
|
||||||
onProtectionViolation?: (violationType: ViolationType) => void;
|
onProtectionViolation?: (violationType: ViolationType) => void;
|
||||||
fallbackSrc?: string;
|
fallbackSrc?: string;
|
||||||
@@ -83,13 +79,6 @@ export interface WatermarkConfig {
|
|||||||
rotation: number;
|
rotation: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FragmentConfig {
|
|
||||||
enabled: boolean;
|
|
||||||
gridSize: number;
|
|
||||||
scramble: boolean;
|
|
||||||
randomSeed?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SteganographyConfig {
|
export interface SteganographyConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
@@ -121,7 +110,6 @@ export interface CanvasProtectionContext {
|
|||||||
originalImageData: ImageData;
|
originalImageData: ImageData;
|
||||||
protectedImageData: ImageData;
|
protectedImageData: ImageData;
|
||||||
watermarkApplied: boolean;
|
watermarkApplied: boolean;
|
||||||
fragmentsScrambled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrintScreenDetectionState {
|
export interface PrintScreenDetectionState {
|
||||||
@@ -181,7 +169,6 @@ export interface ProtectionConfig {
|
|||||||
rendering: {
|
rendering: {
|
||||||
canvas: {
|
canvas: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
fragmentGrid: FragmentConfig;
|
|
||||||
watermark: WatermarkConfig;
|
watermark: WatermarkConfig;
|
||||||
steganography: SteganographyConfig;
|
steganography: SteganographyConfig;
|
||||||
noiseInjection: boolean;
|
noiseInjection: boolean;
|
||||||
@@ -231,9 +218,7 @@ export type ProtectionProps = {
|
|||||||
|
|
||||||
export type CanvasProtectionProps = ProtectionProps & {
|
export type CanvasProtectionProps = ProtectionProps & {
|
||||||
useCanvasRendering?: boolean;
|
useCanvasRendering?: boolean;
|
||||||
fragmentGrid?: boolean;
|
|
||||||
watermarkText?: string;
|
watermarkText?: string;
|
||||||
scrambleFragments?: boolean;
|
|
||||||
invisibleWatermark?: boolean;
|
invisibleWatermark?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user