Security Updates and random improvement.
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.2.0</version>
|
<version>3.5.5</version>
|
||||||
<relativePath/>
|
<relativePath/>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>17</java.version>
|
<java.version>17</java.version>
|
||||||
<testcontainers.version>1.19.3</testcontainers.version>
|
<testcontainers.version>1.21.3</testcontainers.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@@ -56,18 +56,18 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-api</artifactId>
|
<artifactId>jjwt-api</artifactId>
|
||||||
<version>0.12.3</version>
|
<version>0.13.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-impl</artifactId>
|
<artifactId>jjwt-impl</artifactId>
|
||||||
<version>0.12.3</version>
|
<version>0.13.0</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-jackson</artifactId>
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
<version>0.12.3</version>
|
<version>0.13.0</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -89,12 +89,13 @@ public class StoryController {
|
|||||||
@GetMapping("/random")
|
@GetMapping("/random")
|
||||||
public ResponseEntity<StorySummaryDto> getRandomStory(
|
public ResponseEntity<StorySummaryDto> getRandomStory(
|
||||||
@RequestParam(required = false) String searchQuery,
|
@RequestParam(required = false) String searchQuery,
|
||||||
@RequestParam(required = false) List<String> tags) {
|
@RequestParam(required = false) List<String> tags,
|
||||||
|
@RequestParam(required = false) Long seed) {
|
||||||
|
|
||||||
logger.info("Getting random story with filters - searchQuery: {}, tags: {}",
|
logger.info("Getting random story with filters - searchQuery: {}, tags: {}, seed: {}",
|
||||||
searchQuery, tags);
|
searchQuery, tags, seed);
|
||||||
|
|
||||||
Optional<Story> randomStory = storyService.findRandomStory(searchQuery, tags);
|
Optional<Story> randomStory = storyService.findRandomStory(searchQuery, tags, seed);
|
||||||
|
|
||||||
if (randomStory.isPresent()) {
|
if (randomStory.isPresent()) {
|
||||||
StorySummaryDto storyDto = convertToSummaryDto(randomStory.get());
|
StorySummaryDto storyDto = convertToSummaryDto(randomStory.get());
|
||||||
|
|||||||
@@ -676,14 +676,31 @@ public class StoryService {
|
|||||||
* Find a random story based on optional filters.
|
* Find a random story based on optional filters.
|
||||||
* Uses Typesense for consistency with Library search functionality.
|
* Uses Typesense for consistency with Library search functionality.
|
||||||
* Supports text search and multiple tags using the same logic as the Library view.
|
* Supports text search and multiple tags using the same logic as the Library view.
|
||||||
|
* @param searchQuery Optional search query
|
||||||
|
* @param tags Optional list of tags to filter by
|
||||||
|
* @return Optional containing the random story if found
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Optional<Story> findRandomStory(String searchQuery, List<String> tags) {
|
public Optional<Story> findRandomStory(String searchQuery, List<String> tags) {
|
||||||
|
return findRandomStory(searchQuery, tags, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a random story based on optional filters with seed support.
|
||||||
|
* Uses Typesense for consistency with Library search functionality.
|
||||||
|
* Supports text search and multiple tags using the same logic as the Library view.
|
||||||
|
* @param searchQuery Optional search query
|
||||||
|
* @param tags Optional list of tags to filter by
|
||||||
|
* @param seed Optional seed for consistent randomization (null for truly random)
|
||||||
|
* @return Optional containing the random story if found
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Optional<Story> findRandomStory(String searchQuery, List<String> tags, Long seed) {
|
||||||
|
|
||||||
// Use Typesense if available for consistency with Library search
|
// Use Typesense if available for consistency with Library search
|
||||||
if (typesenseService != null) {
|
if (typesenseService != null) {
|
||||||
try {
|
try {
|
||||||
Optional<UUID> randomStoryId = typesenseService.getRandomStoryId(searchQuery, tags);
|
Optional<UUID> randomStoryId = typesenseService.getRandomStoryId(searchQuery, tags, seed);
|
||||||
if (randomStoryId.isPresent()) {
|
if (randomStoryId.isPresent()) {
|
||||||
return storyRepository.findById(randomStoryId.get());
|
return storyRepository.findById(randomStoryId.get());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -402,12 +402,68 @@ public class TypesenseService {
|
|||||||
/**
|
/**
|
||||||
* Get a random story using the same search logic as the Library view.
|
* Get a random story using the same search logic as the Library view.
|
||||||
* This ensures consistency between Library search results and Random Story functionality.
|
* This ensures consistency between Library search results and Random Story functionality.
|
||||||
* Uses offset-based randomization since Typesense v0.25.0 doesn't support _rand() sorting.
|
* Uses Typesense's native _rand() function for efficient randomization with optional seed support.
|
||||||
*/
|
*/
|
||||||
public Optional<UUID> getRandomStoryId(String searchQuery, List<String> tags) {
|
public Optional<UUID> getRandomStoryId(String searchQuery, List<String> tags, Long seed) {
|
||||||
try {
|
try {
|
||||||
String normalizedQuery = (searchQuery == null || searchQuery.trim().isEmpty()) ? "*" : searchQuery.trim();
|
String normalizedQuery = (searchQuery == null || searchQuery.trim().isEmpty()) ? "*" : searchQuery.trim();
|
||||||
|
|
||||||
|
logger.debug("Getting random story with query: '{}', tags: {}, seed: {}", normalizedQuery, tags, seed);
|
||||||
|
|
||||||
|
// Try using Typesense's native _rand() function first
|
||||||
|
try {
|
||||||
|
// Build sort parameter with _rand() function
|
||||||
|
String sortBy = seed != null ? "_rand(" + seed + ")" : "_rand()";
|
||||||
|
|
||||||
|
// Search for a random story using Typesense's native _rand() function
|
||||||
|
SearchParameters searchParameters = new SearchParameters()
|
||||||
|
.q(normalizedQuery)
|
||||||
|
.queryBy("title,description,authorName,seriesName,tagNames")
|
||||||
|
.sortBy(sortBy)
|
||||||
|
.perPage(1); // Only need one random result
|
||||||
|
|
||||||
|
// Add tag filters if provided
|
||||||
|
if (tags != null && !tags.isEmpty()) {
|
||||||
|
String tagFilter = tags.stream()
|
||||||
|
.map(tag -> "tagNames:=" + escapeTypesenseValue(tag))
|
||||||
|
.collect(Collectors.joining(" && "));
|
||||||
|
searchParameters.filterBy(tagFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchResult searchResult = libraryService.getCurrentTypesenseClient().collections(getStoriesCollection())
|
||||||
|
.documents()
|
||||||
|
.search(searchParameters);
|
||||||
|
|
||||||
|
if (searchResult.getHits().isEmpty()) {
|
||||||
|
logger.debug("No stories found matching filters");
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchResultHit hit = searchResult.getHits().get(0);
|
||||||
|
String storyId = (String) hit.getDocument().get("id");
|
||||||
|
logger.debug("Found random story ID: {} using _rand() (seed: {})", storyId, seed);
|
||||||
|
|
||||||
|
return Optional.of(UUID.fromString(storyId));
|
||||||
|
|
||||||
|
} catch (Exception randException) {
|
||||||
|
logger.warn("Failed to use _rand() function, falling back to offset-based randomization: {}", randException.getMessage());
|
||||||
|
|
||||||
|
// Fallback to offset-based randomization if _rand() is not supported
|
||||||
|
return getRandomStoryIdFallback(normalizedQuery, tags, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to get random story with query: '{}', tags: {}", searchQuery, tags, e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback method for random story selection using offset-based randomization with seed support.
|
||||||
|
* Used when Typesense's _rand() function is not available or supported.
|
||||||
|
*/
|
||||||
|
private Optional<UUID> getRandomStoryIdFallback(String normalizedQuery, List<String> tags, Long seed) {
|
||||||
|
try {
|
||||||
// First, get the total count of matching stories
|
// First, get the total count of matching stories
|
||||||
SearchParameters countParameters = new SearchParameters()
|
SearchParameters countParameters = new SearchParameters()
|
||||||
.q(normalizedQuery)
|
.q(normalizedQuery)
|
||||||
@@ -422,21 +478,28 @@ public class TypesenseService {
|
|||||||
countParameters.filterBy(tagFilter);
|
countParameters.filterBy(tagFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Getting random story with query: '{}', tags: {}", normalizedQuery, tags);
|
|
||||||
|
|
||||||
SearchResult countResult = libraryService.getCurrentTypesenseClient().collections(getStoriesCollection())
|
SearchResult countResult = libraryService.getCurrentTypesenseClient().collections(getStoriesCollection())
|
||||||
.documents()
|
.documents()
|
||||||
.search(countParameters);
|
.search(countParameters);
|
||||||
|
|
||||||
long totalHits = countResult.getFound();
|
long totalHits = countResult.getFound();
|
||||||
if (totalHits == 0) {
|
if (totalHits == 0) {
|
||||||
logger.debug("No stories found matching filters");
|
logger.debug("No stories found matching filters in fallback method");
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate random offset within the total hits
|
// Generate random offset with seed support
|
||||||
long randomOffset = (long) (Math.random() * totalHits);
|
long randomOffset;
|
||||||
logger.debug("Total hits: {}, using random offset: {}", totalHits, randomOffset);
|
if (seed != null) {
|
||||||
|
// Use seed to create deterministic randomization
|
||||||
|
java.util.Random random = new java.util.Random(seed);
|
||||||
|
randomOffset = (long) (random.nextDouble() * totalHits);
|
||||||
|
} else {
|
||||||
|
// Use true randomization
|
||||||
|
randomOffset = (long) (Math.random() * totalHits);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Fallback: Total hits: {}, using random offset: {} (seed: {})", totalHits, randomOffset, seed);
|
||||||
|
|
||||||
// Now get the actual story at that offset
|
// Now get the actual story at that offset
|
||||||
SearchParameters storyParameters = new SearchParameters()
|
SearchParameters storyParameters = new SearchParameters()
|
||||||
@@ -459,7 +522,7 @@ public class TypesenseService {
|
|||||||
.search(storyParameters);
|
.search(storyParameters);
|
||||||
|
|
||||||
if (storyResult.getHits().isEmpty()) {
|
if (storyResult.getHits().isEmpty()) {
|
||||||
logger.debug("No stories found in random offset query");
|
logger.debug("No stories found in fallback random offset query");
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,13 +532,13 @@ public class TypesenseService {
|
|||||||
|
|
||||||
SearchResultHit hit = storyResult.getHits().get(indexInPage);
|
SearchResultHit hit = storyResult.getHits().get(indexInPage);
|
||||||
String storyId = (String) hit.getDocument().get("id");
|
String storyId = (String) hit.getDocument().get("id");
|
||||||
logger.debug("Found random story ID: {} at offset {} (page {}, index {})",
|
logger.debug("Found random story ID: {} using fallback method at offset {} (seed: {})",
|
||||||
storyId, randomOffset, storyParameters.getPage(), indexInPage);
|
storyId, randomOffset, seed);
|
||||||
|
|
||||||
return Optional.of(UUID.fromString(storyId));
|
return Optional.of(UUID.fromString(storyId));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed to get random story with query: '{}', tags: {}", searchQuery, tags, e);
|
logger.error("Fallback random story method also failed: {}", e.getMessage());
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ services:
|
|||||||
- storycove-network
|
- storycove-network
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:16-alpine
|
||||||
# No port mapping - only accessible within the Docker network
|
# No port mapping - only accessible within the Docker network
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=storycove
|
- POSTGRES_DB=storycove
|
||||||
@@ -62,7 +62,7 @@ services:
|
|||||||
- storycove-network
|
- storycove-network
|
||||||
|
|
||||||
typesense:
|
typesense:
|
||||||
image: typesense/typesense:0.25.0
|
image: typesense/typesense:29.0
|
||||||
# No port mapping - only accessible within the Docker network
|
# No port mapping - only accessible within the Docker network
|
||||||
environment:
|
environment:
|
||||||
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
|
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
|
||||||
|
|||||||
12
frontend/package-lock.json
generated
12
frontend/package-lock.json
generated
@@ -10,9 +10,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.11.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"dompurify": "^3.0.5",
|
"dompurify": "^3.2.6",
|
||||||
"next": "14.0.0",
|
"next": "14.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
@@ -1372,13 +1372,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.10.0",
|
"version": "1.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.4",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.11.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"dompurify": "^3.0.5",
|
"dompurify": "^3.2.6",
|
||||||
"next": "14.0.0",
|
"next": "14.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
|||||||
2467
package-lock.json
generated
2467
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-code": "^1.0.70",
|
"@anthropic-ai/claude-code": "^1.0.70",
|
||||||
"cheerio": "^1.1.2",
|
"cheerio": "^1.1.2",
|
||||||
"g": "^2.0.1"
|
"g": "^2.0.1",
|
||||||
|
"npm": "^11.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user