fix: address PR review feedback — security, architecture, dead code
Fixes from PR #178 review: Migration fixes: - V18/V19: fix FK references from app_users to users (correct table name) - V18: change annotation_id FK from ON DELETE CASCADE to ON DELETE RESTRICT (block is aggregate root, cascade flows from block, not annotation) Backend fixes: - TranscriptionService.deleteBlock(): remove userId param, delete block first then annotation directly via repository (no ownership check — block owns annotation) - TranscriptionService.sanitizeText(): remove flawed regex HTML stripping, textarea content is plain text by design — just enforce max length - TranscriptionBlockController.requireUserId(): throw DomainException.unauthorized() instead of silently returning null on auth failure - CreateTranscriptionBlockDTO: add @Min/@Positive validation on coordinates - Add @Slf4j logging to TranscriptionService for create/delete operations Frontend fixes: - Delete DocumentBottomPanel.svelte entirely (issue #175 requirement) - Remove redundant mode exclusivity $effect (handled at toggle call sites) - Remove dead handleCommentClick + onCommentClick prop (comments are future work) - Remove quote hint UI (depends on comment feature) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.dto.CreateTranscriptionBlockDTO;
|
||||
import org.raddatz.familienarchiv.dto.ReorderTranscriptionBlocksDTO;
|
||||
import org.raddatz.familienarchiv.dto.UpdateTranscriptionBlockDTO;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.model.AppUser;
|
||||
import org.raddatz.familienarchiv.model.TranscriptionBlock;
|
||||
import org.raddatz.familienarchiv.model.TranscriptionBlockVersion;
|
||||
@@ -47,7 +48,7 @@ public class TranscriptionBlockController {
|
||||
@PathVariable UUID documentId,
|
||||
@RequestBody CreateTranscriptionBlockDTO dto,
|
||||
Authentication authentication) {
|
||||
UUID userId = resolveUserId(authentication);
|
||||
UUID userId = requireUserId(authentication);
|
||||
return transcriptionService.createBlock(documentId, dto, userId);
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ public class TranscriptionBlockController {
|
||||
@PathVariable UUID blockId,
|
||||
@RequestBody UpdateTranscriptionBlockDTO dto,
|
||||
Authentication authentication) {
|
||||
UUID userId = resolveUserId(authentication);
|
||||
UUID userId = requireUserId(authentication);
|
||||
return transcriptionService.updateBlock(documentId, blockId, dto, userId);
|
||||
}
|
||||
|
||||
@@ -67,10 +68,8 @@ public class TranscriptionBlockController {
|
||||
@RequirePermission(Permission.WRITE_ALL)
|
||||
public void deleteBlock(
|
||||
@PathVariable UUID documentId,
|
||||
@PathVariable UUID blockId,
|
||||
Authentication authentication) {
|
||||
UUID userId = resolveUserId(authentication);
|
||||
transcriptionService.deleteBlock(documentId, blockId, userId);
|
||||
@PathVariable UUID blockId) {
|
||||
transcriptionService.deleteBlock(documentId, blockId);
|
||||
}
|
||||
|
||||
@PutMapping("/reorder")
|
||||
@@ -89,14 +88,14 @@ public class TranscriptionBlockController {
|
||||
return transcriptionService.getBlockHistory(documentId, blockId);
|
||||
}
|
||||
|
||||
private UUID resolveUserId(Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) return null;
|
||||
try {
|
||||
AppUser user = userService.findByUsername(authentication.getName());
|
||||
return user != null ? user.getId() : null;
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not resolve user for transcription: {}", e.getMessage());
|
||||
return null;
|
||||
private UUID requireUserId(Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
throw DomainException.unauthorized("Authentication required");
|
||||
}
|
||||
AppUser user = userService.findByUsername(authentication.getName());
|
||||
if (user == null) {
|
||||
throw DomainException.unauthorized("User not found");
|
||||
}
|
||||
return user.getId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.raddatz.familienarchiv.dto;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -8,10 +10,15 @@ import lombok.NoArgsConstructor;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateTranscriptionBlockDTO {
|
||||
@Min(0)
|
||||
private int pageNumber;
|
||||
@Min(0)
|
||||
private double x;
|
||||
@Min(0)
|
||||
private double y;
|
||||
@Positive
|
||||
private double width;
|
||||
@Positive
|
||||
private double height;
|
||||
private String text;
|
||||
private String label;
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.model.DocumentAnnotation;
|
||||
import org.raddatz.familienarchiv.model.TranscriptionBlock;
|
||||
import org.raddatz.familienarchiv.model.TranscriptionBlockVersion;
|
||||
import org.raddatz.familienarchiv.repository.AnnotationRepository;
|
||||
import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
|
||||
import org.raddatz.familienarchiv.repository.TranscriptionBlockVersionRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -30,6 +31,7 @@ public class TranscriptionService {
|
||||
|
||||
private final TranscriptionBlockRepository blockRepository;
|
||||
private final TranscriptionBlockVersionRepository versionRepository;
|
||||
private final AnnotationRepository annotationRepository;
|
||||
private final AnnotationService annotationService;
|
||||
private final DocumentService documentService;
|
||||
|
||||
@@ -69,6 +71,7 @@ public class TranscriptionService {
|
||||
|
||||
TranscriptionBlock saved = blockRepository.save(block);
|
||||
saveVersion(saved, userId);
|
||||
log.info("Created transcription block {} for document {}", saved.getId(), documentId);
|
||||
return saved;
|
||||
}
|
||||
|
||||
@@ -90,11 +93,17 @@ public class TranscriptionService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteBlock(UUID documentId, UUID blockId, UUID userId) {
|
||||
public void deleteBlock(UUID documentId, UUID blockId) {
|
||||
TranscriptionBlock block = getBlock(documentId, blockId);
|
||||
// CASCADE deletes annotation, versions, and comments via DB constraints
|
||||
UUID annotationId = block.getAnnotationId();
|
||||
|
||||
// Block is the aggregate root — delete block first (cascades to versions + comments),
|
||||
// then delete the dependent annotation directly (no ownership check needed)
|
||||
blockRepository.delete(block);
|
||||
annotationService.deleteAnnotation(documentId, block.getAnnotationId(), userId);
|
||||
blockRepository.flush();
|
||||
annotationRepository.deleteById(annotationId);
|
||||
log.info("Deleted transcription block {} and annotation {} for document {}",
|
||||
blockId, annotationId, documentId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -122,13 +131,11 @@ public class TranscriptionService {
|
||||
versionRepository.save(version);
|
||||
}
|
||||
|
||||
private String sanitizeText(String text) {
|
||||
String sanitizeText(String text) {
|
||||
if (text == null) return "";
|
||||
// Strip any HTML tags — textarea content should be plain text only
|
||||
String cleaned = text.replaceAll("<[^>]*>", "");
|
||||
if (cleaned.length() > MAX_TEXT_LENGTH) {
|
||||
cleaned = cleaned.substring(0, MAX_TEXT_LENGTH);
|
||||
if (text.length() > MAX_TEXT_LENGTH) {
|
||||
text = text.substring(0, MAX_TEXT_LENGTH);
|
||||
}
|
||||
return cleaned;
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
CREATE TABLE transcription_blocks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
annotation_id UUID NOT NULL REFERENCES document_annotations(id) ON DELETE CASCADE,
|
||||
annotation_id UUID NOT NULL REFERENCES document_annotations(id) ON DELETE RESTRICT,
|
||||
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL DEFAULT '',
|
||||
label VARCHAR(200),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
created_by UUID REFERENCES app_users(id) ON DELETE SET NULL,
|
||||
updated_by UUID REFERENCES app_users(id) ON DELETE SET NULL,
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
updated_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ CREATE TABLE transcription_block_versions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
block_id UUID NOT NULL REFERENCES transcription_blocks(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
changed_by UUID REFERENCES app_users(id) ON DELETE SET NULL,
|
||||
changed_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
changed_at TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user