Skip to content

Commit 0da83f7

Browse files
authored
Merge pull request #3132 from booklore-app/develop
Merge develop into master for release
2 parents 9f6cd24 + 6705ba4 commit 0da83f7

File tree

540 files changed

+64994
-51661
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

540 files changed

+64994
-51661
lines changed

.github/workflows/develop-pipeline.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
check_name: Backend Test Results
5555

5656
- name: Upload Backend Test Reports
57-
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
57+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
5858
if: always()
5959
with:
6060
name: backend-test-reports
@@ -120,7 +120,7 @@ jobs:
120120
check_name: Frontend Test Results
121121

122122
- name: Upload Frontend Test Reports
123-
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
123+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
124124
if: always()
125125
with:
126126
name: frontend-test-reports

.github/workflows/master-pipeline.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
check_name: Backend Test Results
6767

6868
- name: Upload Backend Test Reports
69-
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
69+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
7070
if: always()
7171
with:
7272
name: backend-test-reports
@@ -132,7 +132,7 @@ jobs:
132132
check_name: Frontend Test Results
133133

134134
- name: Upload Frontend Test Reports
135-
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
135+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
136136
if: always()
137137
with:
138138
name: frontend-test-reports

booklore-api/src/main/java/org/booklore/BookloreApplication.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
@SpringBootApplication
1515
public class BookloreApplication {
1616

17-
static {
18-
System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
19-
}
20-
2117
public static void main(String[] args) {
2218
SpringApplication.run(BookloreApplication.class, args);
2319
}

booklore-api/src/main/java/org/booklore/config/security/SecurityConfig.java

Lines changed: 7 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import jakarta.servlet.DispatcherType;
66
import jakarta.servlet.http.HttpServletResponse;
77
import lombok.AllArgsConstructor;
8-
import org.booklore.util.FileService;
8+
99
import org.springframework.context.annotation.Bean;
1010
import org.springframework.context.annotation.Configuration;
1111
import org.springframework.core.annotation.Order;
@@ -26,26 +26,16 @@
2626
import org.springframework.web.cors.CorsConfigurationSource;
2727
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
2828
import org.springframework.web.client.RestTemplate;
29-
import org.springframework.http.client.SimpleClientHttpRequestFactory;
29+
import org.springframework.http.client.JdkClientHttpRequestFactory;
3030

31-
import java.io.IOException;
32-
import java.net.HttpURLConnection;
31+
import java.net.http.HttpClient;
3332
import java.util.ArrayList;
3433
import java.util.Arrays;
3534
import java.util.List;
3635
import java.util.regex.Pattern;
3736

3837
import lombok.extern.slf4j.Slf4j;
3938

40-
import javax.net.ssl.HttpsURLConnection;
41-
import javax.net.ssl.SNIHostName;
42-
import javax.net.ssl.SSLParameters;
43-
import javax.net.ssl.SSLSocket;
44-
import javax.net.ssl.SSLSocketFactory;
45-
import java.net.InetAddress;
46-
import java.net.Socket;
47-
import java.util.Collections;
48-
4939
@Slf4j
5040
@AllArgsConstructor
5141
@EnableMethodSecurity
@@ -258,71 +248,10 @@ public AuthenticationManager authenticationManager(HttpSecurity http) throws Exc
258248

259249
@Bean("noRedirectRestTemplate")
260250
public RestTemplate noRedirectRestTemplate() {
261-
return new RestTemplate(
262-
new SimpleClientHttpRequestFactory() {
263-
@Override
264-
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
265-
super.prepareConnection(connection, httpMethod);
266-
connection.setInstanceFollowRedirects(false);
267-
String targetHost = FileService.getTargetHost();
268-
if (targetHost != null) {
269-
connection.setRequestProperty("Host", targetHost);
270-
}
271-
if (connection instanceof HttpsURLConnection httpsConnection) {
272-
if (targetHost != null) {
273-
// Set original host for SNI (even if connecting to IP)
274-
SSLSocketFactory defaultFactory = httpsConnection.getSSLSocketFactory();
275-
httpsConnection.setSSLSocketFactory(new SniSSLSocketFactory(defaultFactory, targetHost));
276-
277-
httpsConnection.setHostnameVerifier((hostname, session) -> {
278-
String expectedHost = FileService.getTargetHost();
279-
if (expectedHost != null) {
280-
// Verify certificate against the original expected hostname, even if connecting via IP
281-
return HttpsURLConnection.getDefaultHostnameVerifier().verify(expectedHost, session);
282-
}
283-
// Fallback: use default verifier for the hostname we connected to
284-
return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session);
285-
});
286-
}
287-
}
288-
}
289-
}
290-
);
291-
}
292-
293-
private static class SniSSLSocketFactory extends SSLSocketFactory {
294-
private final SSLSocketFactory delegate;
295-
private final String targetHost;
296-
297-
public SniSSLSocketFactory(SSLSocketFactory delegate, String targetHost) {
298-
this.delegate = delegate;
299-
this.targetHost = targetHost;
300-
}
301-
302-
@Override
303-
public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); }
304-
@Override
305-
public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); }
306-
307-
@Override
308-
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
309-
// Pass targetHost instead of host (which is the IP) so the internal SSLSession gets the correct peer host
310-
Socket socket = delegate.createSocket(s, targetHost, port, autoClose);
311-
if (socket instanceof SSLSocket sslSocket) {
312-
SNIHostName serverName = new SNIHostName(targetHost);
313-
SSLParameters params = sslSocket.getSSLParameters();
314-
params.setServerNames(Collections.singletonList(serverName));
315-
// Explicitly set EndpointIdentificationAlgorithm so Java verifies the certificate against targetHost
316-
params.setEndpointIdentificationAlgorithm("HTTPS");
317-
sslSocket.setSSLParameters(params);
318-
}
319-
return socket;
320-
}
321-
322-
@Override public Socket createSocket(String host, int port) throws IOException { return delegate.createSocket(host, port); }
323-
@Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { return delegate.createSocket(host, port, localHost, localPort); }
324-
@Override public Socket createSocket(InetAddress host, int port) throws IOException { return delegate.createSocket(host, port); }
325-
@Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return delegate.createSocket(address, port, localAddress, localPort); }
251+
HttpClient httpClient = HttpClient.newBuilder()
252+
.followRedirects(HttpClient.Redirect.NEVER)
253+
.build();
254+
return new RestTemplate(new JdkClientHttpRequestFactory(httpClient));
326255
}
327256

328257
@Bean
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.booklore.mobile.controller;
2+
3+
import org.booklore.mobile.dto.MobileAuthorDetail;
4+
import org.booklore.mobile.dto.MobileAuthorSummary;
5+
import org.booklore.mobile.dto.MobilePageResponse;
6+
import org.booklore.mobile.service.MobileAuthorService;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.Parameter;
9+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
10+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
11+
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import lombok.AllArgsConstructor;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.web.bind.annotation.*;
15+
16+
@AllArgsConstructor
17+
@RestController
18+
@RequestMapping("/api/mobile/v1/authors")
19+
@Tag(name = "Mobile Authors", description = "Mobile-optimized endpoints for browsing authors")
20+
public class MobileAuthorController {
21+
22+
private final MobileAuthorService mobileAuthorService;
23+
24+
@Operation(summary = "Get paginated author list",
25+
description = "Retrieve a paginated list of authors with optional filtering by library, search text, and photo availability.")
26+
@ApiResponses({
27+
@ApiResponse(responseCode = "200", description = "Authors retrieved successfully"),
28+
@ApiResponse(responseCode = "403", description = "Access denied")
29+
})
30+
@GetMapping
31+
public ResponseEntity<MobilePageResponse<MobileAuthorSummary>> getAuthors(
32+
@Parameter(description = "Page number (0-indexed)") @RequestParam(required = false, defaultValue = "0") Integer page,
33+
@Parameter(description = "Page size (max 50)") @RequestParam(required = false, defaultValue = "30") Integer size,
34+
@Parameter(description = "Sort field (name, bookCount, recent)") @RequestParam(required = false, defaultValue = "name") String sort,
35+
@Parameter(description = "Sort direction (asc, desc)") @RequestParam(required = false, defaultValue = "asc") String dir,
36+
@Parameter(description = "Filter by library ID") @RequestParam(required = false) Long libraryId,
37+
@Parameter(description = "Search by author name") @RequestParam(required = false) String search,
38+
@Parameter(description = "Filter by photo availability") @RequestParam(required = false) Boolean hasPhoto) {
39+
40+
return ResponseEntity.ok(mobileAuthorService.getAuthors(page, size, sort, dir, libraryId, search, hasPhoto));
41+
}
42+
43+
@Operation(summary = "Get author details",
44+
description = "Retrieve full details for a specific author including description and book count.")
45+
@ApiResponses({
46+
@ApiResponse(responseCode = "200", description = "Author details retrieved successfully"),
47+
@ApiResponse(responseCode = "403", description = "Access denied"),
48+
@ApiResponse(responseCode = "404", description = "Author not found")
49+
})
50+
@GetMapping("/{authorId}")
51+
public ResponseEntity<MobileAuthorDetail> getAuthorDetail(
52+
@Parameter(description = "Author ID") @PathVariable Long authorId) {
53+
54+
return ResponseEntity.ok(mobileAuthorService.getAuthorDetail(authorId));
55+
}
56+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package org.booklore.mobile.controller;
2+
3+
import lombok.AllArgsConstructor;
4+
import org.booklore.mobile.dto.MobileBookSummary;
5+
import org.booklore.mobile.dto.MobilePageResponse;
6+
import org.booklore.mobile.dto.MobileSeriesSummary;
7+
import org.booklore.mobile.service.MobileSeriesService;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
@AllArgsConstructor
12+
@RestController
13+
@RequestMapping("/api/mobile/v1/series")
14+
public class MobileSeriesController {
15+
16+
private final MobileSeriesService mobileSeriesService;
17+
18+
@GetMapping
19+
public ResponseEntity<MobilePageResponse<MobileSeriesSummary>> getSeries(
20+
@RequestParam(required = false, defaultValue = "0") Integer page,
21+
@RequestParam(required = false, defaultValue = "20") Integer size,
22+
@RequestParam(required = false, defaultValue = "recentlyAdded") String sort,
23+
@RequestParam(required = false, defaultValue = "desc") String dir,
24+
@RequestParam(required = false) Long libraryId,
25+
@RequestParam(required = false) String search,
26+
@RequestParam(required = false) String status) {
27+
28+
boolean inProgressOnly = "in-progress".equalsIgnoreCase(status);
29+
30+
MobilePageResponse<MobileSeriesSummary> response = mobileSeriesService.getSeries(
31+
page, size, sort, dir, libraryId, search, inProgressOnly);
32+
33+
return ResponseEntity.ok(response);
34+
}
35+
36+
@GetMapping("/{seriesName}/books")
37+
public ResponseEntity<MobilePageResponse<MobileBookSummary>> getSeriesBooks(
38+
@PathVariable String seriesName,
39+
@RequestParam(required = false, defaultValue = "0") Integer page,
40+
@RequestParam(required = false, defaultValue = "20") Integer size,
41+
@RequestParam(required = false, defaultValue = "seriesNumber") String sort,
42+
@RequestParam(required = false, defaultValue = "asc") String dir,
43+
@RequestParam(required = false) Long libraryId) {
44+
45+
MobilePageResponse<MobileBookSummary> response = mobileSeriesService.getSeriesBooks(
46+
seriesName, page, size, sort, dir, libraryId);
47+
48+
return ResponseEntity.ok(response);
49+
}
50+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.booklore.mobile.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
@Data
10+
@Builder
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
@JsonInclude(JsonInclude.Include.NON_NULL)
14+
public class MobileAuthorDetail {
15+
private Long id;
16+
private String name;
17+
private String description;
18+
private String asin;
19+
private int bookCount;
20+
private boolean hasPhoto;
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.booklore.mobile.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
@Data
10+
@Builder
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
@JsonInclude(JsonInclude.Include.NON_NULL)
14+
public class MobileAuthorSummary {
15+
private Long id;
16+
private String name;
17+
private String asin;
18+
private int bookCount;
19+
private boolean hasPhoto;
20+
}

booklore-api/src/main/java/org/booklore/mobile/dto/MobileBookDetail.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public class MobileBookDetail {
4545
private String primaryFileType;
4646
private List<String> fileTypes;
4747
private List<MobileBookFile> files;
48+
private Instant coverUpdatedOn;
49+
private Instant audiobookCoverUpdatedOn;
4850

4951
private EpubProgress epubProgress;
5052
private PdfProgress pdfProgress;

booklore-api/src/main/java/org/booklore/mobile/dto/MobileBookSummary.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ public class MobileBookSummary {
2828
private Instant lastReadTime;
2929
private Float readProgress;
3030
private String primaryFileType;
31+
private Instant coverUpdatedOn;
32+
private Instant audiobookCoverUpdatedOn;
3133
}

0 commit comments

Comments
 (0)