Skip to content

Commit 9f6cd24

Browse files
authored
Merge pull request #3101 from booklore-app/develop
Merge develop into master for release
2 parents c663b19 + 6402355 commit 9f6cd24

File tree

117 files changed

+2399
-1069
lines changed

Some content is hidden

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

117 files changed

+2399
-1069
lines changed

.github/pull_request_template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ PASTE OUTPUT HERE
7272
> **All boxes must be checked before requesting review.** Incomplete PRs will be closed without review. No exceptions.
7373
7474
- [ ] This PR is linked to an approved issue
75-
- [ ] Code follows project style guidelines and conventions
75+
- [ ] Code follows project [backend and frontend conventions](../CONTRIBUTING.md#backend-conventions)
7676
- [ ] Branch is up to date with `develop` (merge conflicts resolved)
7777
- [ ] I ran the full stack locally (backend + frontend + database) and verified the change works
7878
- [ ] Automated tests added or updated to cover changes (backend **and** frontend)

CONTRIBUTING.md

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,22 @@ Thanks for your interest in contributing to Booklore! Whether you're fixing bugs
99
**Tech Stack:**
1010

1111
- **Frontend:** Angular 20, TypeScript, PrimeNG 19
12-
- **Backend:** Java 21, Spring Boot 3.5
12+
- **Backend:** Java 25, Spring Boot 3.5
1313
- **Authentication:** Local JWT + optional OIDC (e.g., Authentik)
1414
- **Database:** MariaDB
1515
- **Deployment:** Docker-compatible, reverse proxy-ready
1616

1717
## Table of Contents
1818

19+
- [Before You Start](#before-you-start)
1920
- [Where to Start](#where-to-start)
2021
- [Getting Started](#getting-started)
2122
- [Development Setup](#development-setup)
2223
- [Running Tests](#running-tests)
2324
- [Making Changes](#making-changes)
2425
- [Submitting a Pull Request](#submitting-a-pull-request)
25-
- [Code Style](#code-style)
26+
- [Backend Conventions](#backend-conventions)
27+
- [Frontend Conventions](#frontend-conventions)
2628
- [Reporting Bugs](#reporting-bugs)
2729
- [Community & Support](#community--support)
2830
- [Code of Conduct](#code-of-conduct)
@@ -42,7 +44,7 @@ This protects both your time and ours. It ensures that the work is actually want
4244
- No test output pasted in the PR
4345
- Bulk AI-generated changes that clearly haven't been reviewed or tested
4446
- Unsolicited refactors, cleanups, or "improvements" nobody asked for
45-
- PRs over 1000+ changed lines (split them up)
47+
- PRs with 1000+ changed lines (split them up)
4648

4749
## Where to Start
4850

@@ -51,8 +53,6 @@ Not sure where to begin? Look for issues labeled:
5153
- [`good first issue`](https://github.com/booklore-app/booklore/labels/good%20first%20issue) - small, well-scoped tasks ideal for newcomers
5254
- [`help wanted`](https://github.com/booklore-app/booklore/labels/help%20wanted) - tasks where maintainers would appreciate a hand
5355

54-
You can also check the [project roadmap](https://github.com/booklore-app/booklore/projects) for larger initiatives.
55-
5656
---
5757

5858
## Getting Started
@@ -89,7 +89,7 @@ git push origin develop
8989
```
9090
booklore/
9191
├── booklore-ui/ # Angular frontend (TypeScript, PrimeNG)
92-
├── booklore-api/ # Spring Boot backend (Java 21, Gradle)
92+
├── booklore-api/ # Spring Boot backend (Java 25, Gradle)
9393
├── dev.docker-compose.yml # Development Docker stack
9494
├── assets/ # Shared assets (logos, icons)
9595
└── local/ # Local development helpers
@@ -127,8 +127,8 @@ For full control over each component or IDE integration (debugging, hot-reload,
127127

128128
| Tool | Version | Download |
129129
|---------------|---------|----------------------------------------------|
130-
| Java | 21+ | [Adoptium](https://adoptium.net/) |
131-
| Node.js + npm | 18+ | [nodejs.org](https://nodejs.org/) |
130+
| Java | 25+ | [Adoptium](https://adoptium.net/) |
131+
| Node.js + npm | 20+ | [nodejs.org](https://nodejs.org/) |
132132
| MariaDB | 10.6+ | [mariadb.org](https://mariadb.org/download/) |
133133
| Git | latest | [git-scm.com](https://git-scm.com/) |
134134

@@ -182,7 +182,7 @@ ng serve
182182

183183
The UI will be available at http://localhost:4200 with hot-reload enabled.
184184

185-
> If you hit dependency issues, try `npm install --legacy-peer-deps`.
185+
> If you hit dependency issues, try `npm ci --force`.
186186
187187
---
188188

@@ -265,8 +265,8 @@ Before opening your PR:
265265
- [ ] PR is linked to an **approved** issue (PRs without a linked issue will be closed)
266266
- [ ] All tests pass (`./gradlew test` and `ng test`)
267267
- [ ] Actual test output is pasted in the PR description
268-
- [ ] Code follows project conventions (see [Code Style](#code-style))
269-
- [ ] IntelliJ linter shows no errors
268+
- [ ] Code follows project conventions (see [Backend Conventions](#backend-conventions), [Frontend Conventions](#frontend-conventions))
269+
- [ ] No lint errors
270270
- [ ] Branch is up-to-date with `develop`
271271
- [ ] You ran the full stack locally and manually verified the change works
272272
- [ ] PR description includes a screen recording or screenshots proving it works
@@ -276,6 +276,8 @@ Before opening your PR:
276276
- [ ] **PR is reasonably sized.** PRs with 1000+ changed lines will be closed without review. Break large changes into small, focused PRs.
277277
- [ ] **For user-facing features:** submit a companion docs PR at [booklore-docs](https://github.com/booklore-app/booklore-docs)
278278

279+
> When you open your PR on GitHub, a **PR template** will appear. Fill it out completely, including test output and screenshots.
280+
279281
### AI-Assisted Contributions
280282

281283
Contributions using AI tools (Copilot, Claude, ChatGPT, etc.) are welcome, but the quality bar is the same as human-written code. **If you ship it, you own it.**
@@ -293,14 +295,32 @@ We've seen a sharp increase in AI-generated PRs where the contributor clearly ne
293295

294296
---
295297

296-
## Code Style
298+
## Backend Conventions
299+
300+
- Use Spring Data JPA repository methods or JPQL. No native queries unless explicitly approved by a maintainer.
301+
- Constructor injection via Lombok `@AllArgsConstructor`. No `@Autowired`.
302+
- Logging via Lombok `@Slf4j`. No manual `LoggerFactory.getLogger(...)`.
303+
- Throw errors via `ApiError` enum (`ApiError.SOME_ERROR.createException()`). No raw `RuntimeException`.
304+
- Entities use `*Entity` suffix; DTOs drop it (e.g., `BookEntity` vs `Book`).
305+
- Use MapStruct for entity-to-DTO mapping. No hand-written mapping code.
306+
- Security: `@PreAuthorize("@securityUtil.isAdmin()")` or `@CheckBookAccess`. No `@Secured` or `@RolesAllowed`.
307+
- Testing: JUnit 5 + Mockito + AssertJ. `@ExtendWith(MockitoExtension.class)` for unit tests, `@SpringBootTest` only for integration tests.
308+
- Use modern Java features (records, sealed classes, pattern matching, text blocks, etc.).
309+
- No fully qualified class names inline. Always use imports.
310+
- Flyway migrations go in `booklore-api/src/main/resources/db/migration/` with naming `V<number>__<Description>.sql`.
311+
- Never modify a released migration. Always create a new migration file for changes.
312+
- Use idempotent guards in migrations (`CREATE TABLE IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`, `DROP ... IF EXISTS` before re-creating).
313+
314+
---
315+
316+
## Frontend Conventions
297317

298-
| Area | Convention |
299-
|------------|--------------------------------------------------------------------|
300-
| Angular | Follow the [official style guide](https://angular.dev/style-guide) |
301-
| Java | Modern Java 21 features, clean structure |
302-
| Formatting | Use IntelliJ IDEA's built-in linter |
303-
| UI | SCSS with PrimeNG components |
318+
- Follow the [Angular style guide](https://angular.dev/style-guide).
319+
- All components are standalone. No NgModules.
320+
- Use `inject()` for dependency injection. No constructor injection.
321+
- PrimeNG for UI components. SCSS for styling. Transloco for i18n.
322+
- Testing with Vitest (not Karma/Jasmine).
323+
- All UI features must be responsive (desktop + mobile).
304324

305325
---
306326

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ WORKDIR /angular-app
55

66
COPY ./booklore-ui/package.json ./booklore-ui/package-lock.json ./
77
RUN --mount=type=cache,target=/root/.npm \
8-
npm config set registry http://registry.npmjs.org/ \
8+
npm config set registry https://registry.npmjs.org/ \
99
&& npm ci --force
1010

1111
COPY ./booklore-ui /angular-app/
@@ -54,7 +54,7 @@ LABEL org.opencontainers.image.title="BookLore" \
5454
org.opencontainers.image.licenses="GPL-3.0" \
5555
org.opencontainers.image.base.name="docker.io/library/eclipse-temurin:25-jre-alpine"
5656

57-
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -XX:+UseContainerSupport -XX:+UseCompactObjectHeaders -XX:MaxRAMPercentage=75.0"
57+
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
5858

5959
ARG TARGETARCH
6060
RUN apk update && apk add --no-cache su-exec libstdc++ libgcc && \

Dockerfile.ci

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ LABEL org.opencontainers.image.title="BookLore" \
1616
org.opencontainers.image.licenses="GPL-3.0" \
1717
org.opencontainers.image.base.name="docker.io/library/eclipse-temurin:25-jre-alpine"
1818

19-
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -XX:+UseContainerSupport -XX:+UseCompactObjectHeaders -XX:MaxRAMPercentage=75.0"
19+
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
2020

2121
ARG TARGETARCH
2222
RUN apk update && apk add --no-cache su-exec libstdc++ libgcc

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ volumes:
212212
| 💬 **Come hang out** | [Discord Server](https://discord.gg/Ee5hd458Uz) |
213213
214214
> [!WARNING]
215-
> **Before opening a PR:** Open an issue first and get maintainer approval. PRs without a linked issue, without screenshots/video proof, or without pasted test output will be closed. AI-assisted contributions are welcome, but you must run, test, and understand every line you submit. See the [Contributing Guide](CONTRIBUTING.md) for full details.
215+
> **Before opening a PR:** Open an issue first and get maintainer approval. PRs without a linked issue, without screenshots/video proof, or without pasted test output will be closed. All code must follow project [backend](CONTRIBUTING.md#backend-conventions) and [frontend](CONTRIBUTING.md#frontend-conventions) conventions. AI-assisted contributions are welcome, but you must run, test, and understand every line you submit. See the [Contributing Guide](CONTRIBUTING.md) for full details.
216216
217217
---
218218

booklore-api/build.gradle

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ plugins {
22
id 'java'
33
id 'org.springframework.boot' version '4.0.3'
44
id 'io.spring.dependency-management' version '1.1.7'
5-
id 'org.hibernate.orm' version '7.2.4.Final'
5+
id 'org.hibernate.orm' version '7.2.5.Final'
66
id 'com.github.ben-manes.versions' version '0.53.0'
77
id 'jacoco'
88
}
@@ -44,7 +44,7 @@ dependencies {
4444
// --- Database & Migration ---
4545
implementation 'org.mariadb.jdbc:mariadb-java-client:3.5.7'
4646
implementation 'org.springframework.boot:spring-boot-starter-flyway'
47-
implementation 'org.flywaydb:flyway-mysql:12.0.1'
47+
implementation 'org.flywaydb:flyway-mysql:12.0.2'
4848

4949
// --- Security & Authentication ---
5050
implementation 'io.jsonwebtoken:jjwt-api:0.13.0'
@@ -65,18 +65,18 @@ dependencies {
6565
implementation 'com.github.jai-imageio:jai-imageio-jpeg2000:1.4.0'
6666

6767
// --- TwelveMonkeys ImageIO ---
68-
implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.13.0'
69-
implementation 'com.twelvemonkeys.imageio:imageio-tiff:3.13.0'
70-
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.13.0'
71-
implementation 'com.twelvemonkeys.imageio:imageio-bmp:3.13.0'
68+
implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.13.1'
69+
implementation 'com.twelvemonkeys.imageio:imageio-tiff:3.13.1'
70+
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.13.1'
71+
implementation 'com.twelvemonkeys.imageio:imageio-bmp:3.13.1'
7272

7373
implementation 'io.documentnode:epub4j-core:4.2.3'
7474

7575
// --- Audio Metadata (Audiobook Support) ---
7676
implementation 'net.jthink:jaudiotagger:3.0.1'
7777

7878
// --- UNRAR Support ---
79-
implementation 'com.github.junrar:junrar:7.5.7'
79+
implementation 'com.github.junrar:junrar:7.5.8'
8080

8181
// --- JSON & Web Scraping ---
8282
implementation 'org.jsoup:jsoup:1.22.1'
@@ -99,7 +99,7 @@ dependencies {
9999
implementation 'org.freemarker:freemarker:2.3.34'
100100

101101
// --- Jackson 3 ---
102-
implementation platform('tools.jackson:jackson-bom:3.0.4')
102+
implementation platform('tools.jackson:jackson-bom:3.1.0')
103103
implementation 'tools.jackson.core:jackson-core'
104104
implementation 'tools.jackson.core:jackson-databind'
105105
implementation 'tools.jackson.module:jackson-module-blackbird'

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ public SecurityFilterChain jwtApiSecurityChain(HttpSecurity http) throws Excepti
222222
.headers(headers -> headers
223223
.referrerPolicy(referrer -> referrer.policy(
224224
ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
225+
.contentTypeOptions(contentType -> {})
225226
)
226227
.authorizeHttpRequests(auth -> auth
227228
.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll()
@@ -242,6 +243,7 @@ public SecurityFilterChain staticResourcesSecurityChain(HttpSecurity http) throw
242243
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
243244
.referrerPolicy(referrer -> referrer.policy(
244245
ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
246+
.contentTypeOptions(contentType -> {})
245247
)
246248
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
247249
return http.build();

booklore-api/src/main/java/org/booklore/controller/AudiobookReaderController.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public ResponseEntity<AudiobookInfo> getAudiobookInfo(
4646
@ApiResponse(responseCode = "200", description = "Full audio file returned")
4747
@ApiResponse(responseCode = "206", description = "Partial content returned (range request)")
4848
@ApiResponse(responseCode = "416", description = "Range not satisfiable")
49+
@CheckBookAccess(bookIdParam = "bookId")
4950
@GetMapping("/{bookId}/stream")
5051
public void streamAudiobook(
5152
@Parameter(description = "ID of the book") @PathVariable Long bookId,
@@ -62,6 +63,7 @@ public void streamAudiobook(
6263
@ApiResponse(responseCode = "200", description = "Full track file returned")
6364
@ApiResponse(responseCode = "206", description = "Partial content returned (range request)")
6465
@ApiResponse(responseCode = "416", description = "Range not satisfiable")
66+
@CheckBookAccess(bookIdParam = "bookId")
6567
@GetMapping("/{bookId}/track/{trackIndex}/stream")
6668
public void streamTrack(
6769
@Parameter(description = "ID of the book") @PathVariable Long bookId,
@@ -79,6 +81,7 @@ public void streamTrack(
7981
"Uses token query parameter for authentication.")
8082
@ApiResponse(responseCode = "200", description = "Cover art returned successfully")
8183
@ApiResponse(responseCode = "404", description = "No embedded cover art found")
84+
@CheckBookAccess(bookIdParam = "bookId")
8285
@GetMapping("/{bookId}/cover")
8386
public ResponseEntity<byte[]> getEmbeddedCover(
8487
@Parameter(description = "ID of the book") @PathVariable Long bookId,

booklore-api/src/main/java/org/booklore/controller/AuthorController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.swagger.v3.oas.annotations.Parameter;
1313
import io.swagger.v3.oas.annotations.tags.Tag;
1414
import io.swagger.v3.oas.annotations.responses.ApiResponse;
15+
import jakarta.validation.Valid;
1516
import lombok.AllArgsConstructor;
1617
import org.springframework.http.MediaType;
1718
import org.springframework.http.ResponseEntity;
@@ -69,7 +70,7 @@ public ResponseEntity<AuthorDetails> getAuthorDetails(
6970
@PutMapping("/{authorId}")
7071
public ResponseEntity<AuthorDetails> updateAuthor(
7172
@Parameter(description = "ID of the author") @PathVariable long authorId,
72-
@RequestBody AuthorUpdateRequest request) {
73+
@RequestBody @Valid AuthorUpdateRequest request) {
7374
return ResponseEntity.ok(authorMetadataService.updateAuthor(authorId, request));
7475
}
7576

@@ -101,7 +102,7 @@ public ResponseEntity<List<AuthorSearchResult>> searchAuthorMetadata(
101102
@PostMapping("/{authorId}/match")
102103
public ResponseEntity<AuthorDetails> matchAuthor(
103104
@Parameter(description = "ID of the author") @PathVariable long authorId,
104-
@RequestBody AuthorMatchRequest request) {
105+
@RequestBody @Valid AuthorMatchRequest request) {
105106
return ResponseEntity.ok(authorMetadataService.matchAuthor(authorId, request));
106107
}
107108

booklore-api/src/main/java/org/booklore/controller/BookController.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,20 @@
3636
import jakarta.validation.Valid;
3737
import jakarta.validation.constraints.Max;
3838
import jakarta.validation.constraints.Min;
39+
import jakarta.validation.constraints.Size;
3940
import lombok.AllArgsConstructor;
4041
import org.springframework.core.io.Resource;
4142
import org.springframework.http.ResponseEntity;
4243
import org.springframework.security.access.prepost.PreAuthorize;
44+
import org.springframework.validation.annotation.Validated;
4345
import org.springframework.web.bind.annotation.*;
4446

4547
import java.util.List;
4648
import java.util.Set;
4749

4850
@Tag(name = "Books", description = "Endpoints for managing books, their metadata, progress, and recommendations")
4951
@RequestMapping("/api/v1/books")
52+
@Validated
5053
@RestController
5154
@AllArgsConstructor
5255
public class BookController {
@@ -190,7 +193,7 @@ public ResponseEntity<BookViewerSettings> getBookViewerSettings(
190193
@PutMapping("/{bookId}/viewer-setting")
191194
@CheckBookAccess(bookIdParam = "bookId")
192195
public ResponseEntity<Void> updateBookViewerSettings(
193-
@Parameter(description = "Viewer settings to update") @RequestBody BookViewerSettings bookViewerSettings,
196+
@Parameter(description = "Viewer settings to update") @RequestBody @Valid BookViewerSettings bookViewerSettings,
194197
@Parameter(description = "ID of the book") @PathVariable long bookId) {
195198
bookService.updateBookViewerSetting(bookId, bookViewerSettings);
196199
return ResponseEntity.noContent().build();
@@ -237,7 +240,7 @@ public List<BookStatusUpdateResponse> updateReadStatus(@RequestBody @Valid ReadS
237240
})
238241
@PostMapping("/reset-progress")
239242
public ResponseEntity<List<BookStatusUpdateResponse>> resetProgress(
240-
@Parameter(description = "List of book IDs to reset progress for") @RequestBody List<Long> bookIds,
243+
@Parameter(description = "List of book IDs to reset progress for") @RequestBody @Size(max = 500) List<Long> bookIds,
241244
@Parameter(description = "Type of progress reset") @RequestParam ResetProgressType type) {
242245
if (bookIds == null || bookIds.isEmpty()) {
243246
throw ApiError.GENERIC_BAD_REQUEST.createException("No book IDs provided");
@@ -260,7 +263,7 @@ public ResponseEntity<List<PersonalRatingUpdateResponse>> updatePersonalRating(
260263
})
261264
@PostMapping("/reset-personal-rating")
262265
public ResponseEntity<List<PersonalRatingUpdateResponse>> resetPersonalRating(
263-
@Parameter(description = "List of book IDs to reset personal rating for") @RequestBody List<Long> bookIds) {
266+
@Parameter(description = "List of book IDs to reset personal rating for") @RequestBody @Size(max = 500) List<Long> bookIds) {
264267
if (bookIds == null || bookIds.isEmpty()) {
265268
throw ApiError.GENERIC_BAD_REQUEST.createException("No book IDs provided");
266269
}

0 commit comments

Comments
 (0)